Скрипт для определения, доступна ли хранимая процедура только для чтения или для чтения-записи - PullRequest
0 голосов
/ 24 февраля 2012

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

Я уже написал свой собственный сценарий, но я получаю точность только ~ 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 символов.Я мог бы фактически закончить с комбинацией предложений.Получите список процедур, возвращаемых этим запросом, а затем попробуйте предложение Arrons и посмотрите, смогу ли я отсеять еще больше.Я был бы счастлив с точностью 95%.

Я дам еще один день или около того, чтобы посмотреть, смогу ли я получить какие-либо дальнейшие предложения, но пока большое спасибо.

[ОКОНЧАТЕЛЬНОЕ РЕДАКТИРОВАНИЕ] Хорошо, вот что я в итоге сделал, и похоже, что я получаю точность не менее 95%, может быть выше.Я пытался удовлетворить любой сценарий, который мог придумать.

Я записал хранимые процедуры в файлы и написал приложение ac # 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("dbo.@")) && (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*);

Ответы [ 4 ]

4 голосов
/ 24 февраля 2012

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%? Что вы собираетесь делать с информацией, когда у вас есть два (или три?) Списка?

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

2 голосов
/ 24 февраля 2012

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

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

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

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

1 голос
/ 24 февраля 2012

Вы можете попробовать объединить sys.sql_modules с табличной функцией парсинга слов. РЕДАКТИРОВАТЬ: переименовал UDF в fnParseSQLWords, который идентифицирует комментарии РЕДАКТИРОВАТЬ: добавил условие в ПРАВУЮ строку и изменил все varchar на nvarchar РЕДАКТИРОВАТЬ: Добавлено и w.id > 1; в основной оператор выбора, чтобы избежать попадания в ведущий 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='%'+@delimiter;
    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)-@pi);
        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 голосов
/ 24 февраля 2012

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

я бы не стал слишком долго об этом думать, хотя ...

...