2012-02-23 1 views
0

У меня есть требование провести аудит всех наших хранимых процедур, тысячи из них и определить, какие из них только для чтения или чтения-записи. Мне было интересно, знает ли кто-нибудь хороший способ сделать это точно.Сценарий, чтобы определить, является ли хранимая процедура только для чтения или чтения-записи.

Я написал свой собственный сценарий до сих пор, но я получаю только точность ~ 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*); 
+0

А что, если хранимая процедура записывается в другую базу данных или использует «RAISERROR WITH LOG» или XP_REGWRITE или записывает в файл или отправляет электронное письмо? По сути, вы пытаетесь использовать очень широкую сеть, и я не думаю, что для этого в SQL Server есть какие-то ярлыки. –

+0

@AaronBertrand Точно, поэтому я разместил этот вопрос – Ryk

+0

К сожалению, ответ будет отрицательным (и я не предлагаю вам не задавать вопрос). Как вы уже знали, вы можете искать определенные вещи в тексте хранимой процедуры, чтобы сделать некоторые обоснованные предположения о том, что она делает. Однако это сценарий «доверять, но проверять». RegEx и сопоставление образцов будут указывать только количество, а не качество. Также есть все виды дополнительных переменных, которые еще не были подняты, например, обнаружено ли обнаруженное слово в комментарии, имя параметра или переменной, строковый литерал и т. Д. –

ответ

1

Вы можете попробовать комбинировать sys.sql_modules со словом-разборной табличной функцией. EDIT: переименован в UDF в fnParseSQLWords, который идентифицирует комментарии EDIT: добавлено условие для строки RIGHT и изменено все varchar на nvarchar EDIT: Добавлено и w.id > 1; в основной оператор select, чтобы избежать хитов на ведущем CREATE PROC при фильтрации на CREATE ,

create function [dbo].[fnParseSQLWords](@str nvarchar(max), @delimiter nvarchar(30)='%[^a-zA-Z0-9\_]%') 
returns @result table(id int identity(1,1), bIsComment bit, word nvarchar(max)) 
begin 
    if left(@delimiter,1)<>'%' set @delimiter='%'[email protected]; 
    if right(@delimiter,1)<>'%' set @delimiter+='%'; 
    set @str=rtrim(@str); 
    declare @pi int=PATINDEX(@delimiter,@str); 
    declare @s2 nvarchar(2)=substring(@str,@pi,2); 
    declare @bLineComment bit=case when @s2='--' then 1 else 0 end; 
    declare @bBlockComment bit=case when @s2='/*' then 1 else 0 end; 

    while @pi>0 begin  
     insert into @result select case when (@bLineComment=1 or @bBlockComment=1) then 1 else 0 end 
      , LEFT(@str,@pi-1) where @pi>1; 
     set @s2=substring(@str,@pi,2); 
     set @str=RIGHT(@str,len(@str)[email protected]); 
     set @pi=PATINDEX(@delimiter,@str); 
     set @bLineComment=case when @s2='--' then 1 else @bLineComment end; 
     set @bBlockComment=case when @s2='/*' then 1 else @bBlockComment end; 
     set @bLineComment=case when left(@s2,1) in (char(10),char(13)) then 0 else @bLineComment end; 
     set @bBlockComment=case when @s2='*/' then 0 else @bBlockComment end; 
    end 

    insert into @result select case when (@bLineComment=1 or @bBlockComment=1) then 1 else 0 end 
     , @str where LEN(@str)>0; 
    return; 
end 
GO 

-- List all update procedures 
select distinct ProcName=p.name --, w.id, w.bIsComment, w.word 
from sys.sql_modules m 
inner join sys.procedures p on p.object_id=m.object_id 
cross apply dbo.fnParseSQLWords(m.[definition], default) w 
where w.word in ('INSERT','UPDATE','DELETE','INTO','CREATE','DROP','ALTER','TRUNCATE') 
and w.bIsComment=0 
and w.id > 1; 
GO 
+0

Теперь работает над добавлением строки комментария и обнаружения блоков. Оставайтесь с нами ... –

+0

Можете ли вы объяснить, как это находит слова типа INSERT иначе, чем сценарий в вопросе (при условии, что OP изменяет его использование sys.sql_modules)? –

+0

Это выглядит многообещающе, я буду продолжать проверять – Ryk

4

SQL Server не хранит свойство, атрибут или другие метаданные, которые диктует ли хранимая процедура выполняет любые операции записи. Я бы сказал, что вы можете отсеять все хранимые процедуры, которые не содержат строки, как:

INTO 
CREATE%TABLE 
DELETE 
INSERT 
UPDATE 
TRUNCATE 
OUTPUT 

Это не исчерпывающий список, только несколько экспромтом. Но, конечно, это будет иметь несколько ложных срабатываний, потому что некоторые из оставшихся процедур могут иметь эти слова естественным образом (например, хранимая процедура, называемая «GetIntolerables»). Вам придется выполнить какой-то ручной анализ тех, которые остаются, чтобы определить, действительно ли эти ключевые слова используются по назначению или если они всего лишь побочный эффект. Вы также не сможете сказать, действительно ли процедура, создающая таблицу #temp, делает это только для чтения (и хотя вы объяснили это немного в своем вопросе, я не понимаю, является ли это «удар» или нет).

В SQL Server 2012 вы можете получить немного ближе или хотя бы идентифицировать хранимые процедуры, которые не возвращают набор результатов (подразумевается, что они должны что-то делать). Вы можете написать динамический запрос так:

SELECT QUOTENAME(OBJECT_SCHEMA_NAME(p.[object_id])) + '.' + QUOTENAME(p.name) 
FROM sys.procedures AS p OUTER APPLY 
sys.dm_exec_describe_first_result_set_for_object(p.[object_id], 1) AS d 
WHERE d.name IS NULL; 

Одна из проблем в том, что если ваша процедура имеет любое разветвление в ней, которые зависят от входных параметров, времени суток, состояния системы, данных в таблице, и т.д. то он может не точно отражать то, что он делает. Но это может помочь немного уменьшить список. Он также может возвращать ложные срабатывания для хранимых процедур, которые вставляются в таблицу и возвращают значение идентификатора с использованием SELECT, такого рода вещи.

В более ранних версиях вы можете сделать что-то подобное с SET FMTONLY ON, но в этом случае вам придется выполнять все процедуры, и это будет громоздко, потому что вам также нужно знать о любых требуемых параметрах (оба входы и выходы) и соответственно установить их. Процесс оценки гораздо более ручной, и он все еще подвержен проблеме параметров, описанной выше.

Какой метод вы используете сейчас, чтобы добраться до 85%? Что вы собираетесь делать с информацией, если у вас есть два (или три?) Списка?

Я не вижу никаких ярлыков для этого. В идеальном мире ваше соглашение об именах будет указывать на то, что хранимые процедуры должны быть точно названы для того, что они делают, и вы должны сразу же разделить их (с некоторыми границами). В его нынешнем виде кажется, что вы смотрите на камеру движения и пытаетесь определить, какие автомобили могут иметь оружие под сиденьем водителя.

+0

Может захотеть добавить 'DROP' и' ALTER' там. Я добавил 'CREATE', так как он может также включить индексы. – JNK

+0

@JNK спасибо, я не хотел, чтобы это был исчерпывающий список. Я предполагаю, что у него есть список, который довел его до 85%. –

+0

@AaronBertrand - Я обновил свой вопрос с тем, что я сделал – Ryk

2

Есть несколько ключевых слов, вы можете проверить в sys.sql_modules:

  • UPDATE
  • INSERT
  • INTO
  • DELETE
  • CREATE
  • DROP
  • ALTER
  • TRUNCATE

Если он не содержит каких-либо из них, я не могу придумать, как это писать в базу данных, если его через другой суб-прок или функции (которая будет содержать один этих слов).

После этого вам нужно будет проверить индивидуальность, чтобы убедиться, что это не таблица #temp. Вам также понадобится сделать второй проход, чтобы продолжать поиск объектов, содержащих их в других объектах.

+1

Добавлено как комментарий, поэтому я не краду его из aaron- также 'OUTPUT', я забыл об этом – JNK

+1

Thief! Сдай себя! –

-1

радикальным решением было бы разобрать все процедуры и вставить вызов функции, которая создает моментальный снимок базы данных в первой строке. последняя строка создаст другую и сравнит ее с первой. если они разные, вы сделали вызов для процедуры записи. вы не можете сделать это на производстве, и вам придется либо называть все свои тестовые примеры, либо воспроизводить журнал SQL-сервера.

Я бы не думал слишком долго об этом, tho ...

+0

Ничего себе. Мне это вообще не кажется фантастической идеей. Можете ли вы даже объяснить, как построить список вызовов, включая все необходимые параметры? Как вы знаете значения параметров, чтобы убедиться, что процедура выполнена правильно? И какая у вас система, где вы могли бы создать тысячи снимков? –

+0

Я видел инструменты, которые записывают каждый вызов sql-сервера, просто забыл, что он вызвал. оставьте это на несколько дней/недель, и у вас есть хороший набор тестов. но я упомянул, что он радикален и не стоит слишком много думать, так что давайте не будем, я уже получил свой взбив, чтобы предложить такое безумие. – mindandmedia

+0

Вы можете воспроизвести трассировку (используя профилировщик или сторонние инструменты), или в SQL Server 2012 используйте утилиту распределенного воспроизведения, но я все же считаю, что монументально невозможная часть этой работы - это толчок вашего предложения: (a) сделать предположения на основе параметров, которые использовались в течение периода трассировки, и (б) создания и сравнения тысяч снимков. –

Смежные вопросы