Найти:

Парсинг писем ФСТЭК России

Вот уже более полутора лет как ФСТЭК России информирует нас посредством входящей корреспонденции об уязвимостях операционных систем и программного обеспечения и советует принять необходимые меры для их устранения. За это, конечно, выражаю благодарность ответственным сотрудникам. Вот только компенсирующие меры, которые рекомендуется принимать в случае отсутствия возможности обновления ПО (а с обновлением некоторого ПО могут возникнуть серьёзные трудности) часто дублируются.

Чтобы исключить рутинные процессы по подготовке отчётов о принятых мерах я написал скрипт на Python.

Что требуется для работы скрипта? В первую очередь это текстовый документ, содержащий подготовленный ранее отчёт в виде таблицы. Пример таблицы привожу на скриншоте:

Второй компонент — это письмо ФСТЭК в формате PDF.

Ну и, конечно, не забудьте установить через «pip install …» все используемые в скрипте библиотеки. Самая важная из них — это «python-docx».

Код обновлён 12.02.2024.

# Импорт необходимых библиотек
from PyPDF2 import PdfReader as pypdf
from re import findall as regsearch
from re import sub as resub
from docx import Document
from docx.shared import Cm, Pt
from docx.oxml.shared import OxmlElement
from docx.oxml.ns import qn
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_ALIGN_VERTICAL
from os import sep

# Функция определения отступов в ячейке таблицы
def set_cell_margins(cell, **kwargs):
    tc = cell._tc
    tcPr = tc.get_or_add_tcPr()
    tcMar = OxmlElement('w:tcMar')
    for m in ["top", "start", "bottom", "end"]:
        if m in kwargs:
            node = OxmlElement("w:{}".format(m))
            node.set(qn('w:w'), str(kwargs.get(m)))
            node.set(qn('w:type'), 'dxa')
            tcMar.append(node)
    tcPr.append(tcMar)

# Функция определения повторяющейся шапки таблицы
def set_repeat_table_header(row):
    tr = row._tr
    trPr = tr.get_or_add_trPr()
    tblHeader = OxmlElement('w:tblHeader')
    tblHeader.set(qn('w:val'), "true")
    trPr.append(tblHeader)
    return row

# Функция запрета разрыва строки таблицы по страницам
def keep_table_on_one_page(table):
    tags = table._element.xpath('./w:tr[position() < last()]/w:tc/w:p')
    for tag in tags:
        ppr = tag.get_or_add_pPr()
        ppr.keepNext_val = True

# Письмо с рекомендациями
Letter = input().replace("file://","")
# Отчёт по мерам
Report = "/home/juice/Отчет по ЗИ/Мероприятия по повышению защищенности ИИ.docx"

recoms = {}

# Запись текста письма в переменную
with open(Letter, "rb") as pdf_file:
    Text = ""
    read_pdf = pypdf(pdf_file)
    for page in read_pdf.pages:
        Text += page.extract_text()
    Text = resub("\\d+\\n"," ",Text)
    Text = Text.replace("\n", " ")
    Text = resub(" +", " ", Text).strip()

# Поиск описанных уязвимостей в тексте письма
Vulns = regsearch("Уязвимость.*?CVSS.*?\\)", Text)
Meas = []

# Поиск рекомендованных мер в тексте письма
for num,vul in enumerate(Vulns):
    text = Text.split(vul)[1].split(str(num+2)+". Уязвимость")[0]
    if "В случае невозможности установки обновления программного обеспечения" in text:
        meas = text.split("В случае невозможности установки обновления программного обеспечения")[1]
    elif " рекомендуется" in text:
        meas = text.split(" рекомендуется ")[1]
    else:
        meas = ""
    meas_cor = []
    if "компенсирующие меры" in meas:
        meas = meas.split("компенсирующие меры: ")[1].split(". "+str(num+2)+".")[0].split(";")
        for m in meas:
            m = m.strip().split(". "+str(num+2)+".")[0]
            m = m[:1].upper() + m[1:]
            meas_cor.append(m)
    else:
        for word in ["необходимо", "рекомендуется"]:
            if word in meas:
                if ". "+str(num+2)+"." in meas.split(word)[1]:
                    meas = meas.split(word)[1].split(". "+str(num+2)+".")[0]
                else:
                    meas = meas.rsplit(word)[1]
                break
        meas = meas.strip()
        meas = meas[:1].upper() + meas[1:]
        meas_cor.append(meas)
        
    Meas.append(meas_cor)
    del text, meas, meas_cor

# Исключение из рекомендаций пункта о сроке информирования
if "По результатам" in Meas[-1][-1]:
    lmeas = Meas[-1][-1].split(" По результатам")[0]
    Meas[-1][-1] = lmeas[:-1]

# Чтение выполненных рекомендаций из файла отчёта
Base = Document(Report)
for table in Base.tables:
	for row in table.rows[1:]:
		if len(row.cells) == 3 and row.cells[1].text != row.cells[2].text and row.cells[2].text != "" and "отсутствие" not in row.cells[2].text and "не применя" not in row.cells[2].text:
			recoms[resub(" +", " ", row.cells[1].text.replace("\n", " ")).strip().lower()] = resub(" +", " ",row.cells[2].text.replace("\n", " ")).strip()

# Создание документа для нового отчёта
Outdoc = Document()

# Определение полей страницы
section = Outdoc.sections[0]
section.top_margin = Cm(1)
section.bottom_margin = Cm(1)
section.left_margin = Cm(2)
section.right_margin = Cm(1)

# Создание таблицы и шапки таблицы
Table = Outdoc.add_table(rows=1, cols=3)
Table.style = "Table Grid"
Table.rows[0].cells[0].text = "№ п/п"
Table.rows[0].cells[1].text = "Рекомендации ФСТЭК России"
Table.rows[0].cells[2].text = "Мероприятия"
set_repeat_table_header(Table.rows[0])

# Определение стилей шапки таблицы
for cell in Table.rows[0].cells:
    cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
    set_cell_margins(cell, top=50, start=50, bottom=50, end=50)
    for paragraph in cell.paragraphs:
        paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
        paragraph.paragraph_format.line_spacing = 1
        paragraph.paragraph_format.space_before = paragraph.paragraph_format.space_after = Pt(1)
        for run in paragraph.runs:
            run.font.size = Pt(11)
            run.font.bold = True
            run.font.name = "PT Astra Serif"

N = 0

# Запись в таблицу описания уязвимостей и определение стилей
for vul, meas in zip(Vulns, Meas):
    N += 1
    data_row = Table.add_row().cells
    data_row[0].text = str(N)+"."
    data_row[1].text = vul
    data_row[1].merge(data_row[2])
    for i in range(len(data_row)):
        data_row[i].vertical_alignment = WD_ALIGN_VERTICAL.CENTER
        set_cell_margins(data_row[i], top=50, start=50, bottom=50, end=50)
        for paragraph in data_row[i].paragraphs:
            paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
            paragraph.paragraph_format.line_spacing = 1
            paragraph.paragraph_format.space_before = paragraph.paragraph_format.space_after = Pt(1)
            for run in paragraph.runs:
                run.font.size = Pt(11)
                run.font.bold = True
                run.font.name = "PT Astra Serif"
            
    NN = 0

# Запись в таблицу рекомендаций по устранению уязвимости    
    for m in meas:
        NN += 1
        data_subrow = Table.add_row().cells
        data_subrow[0].text = str(N)+"."+str(NN)+"."
        data_subrow[1].text = m

# Поиск выполненных рекомендаций в отчёте      
        if m.lower() in recoms.keys():
            data_subrow[2].text = recoms[m.lower()]

# Определение стилей ячеек с рекомендациями        
        for i in range(len(data_subrow)):
            data_subrow[i].vertical_alignment = WD_ALIGN_VERTICAL.CENTER
            set_cell_margins(data_subrow[i], top=50, start=50, bottom=50, end=50)
            for paragraph in data_subrow[i].paragraphs:
                paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
                paragraph.paragraph_format.line_spacing = 1
                paragraph.paragraph_format.space_before = paragraph.paragraph_format.space_after = Pt(1)
                for run in paragraph.runs:
                    run.font.size = Pt(11)
                    run.font.bold = False
                    run.font.name = "PT Astra Serif"

# Определение шапки таблицы и ширины столбцов        
#keep_table_on_one_page(Table)    
Table.columns[0].width = Cm(0.79)
Table.columns[1].width = Cm(8.57)
Table.columns[2].width = Cm(8.60)

Outdoc.save(Letter.split(sep)[-1].split(".docx")[-1]+" — отчёт.docx")


Оставить комментарий

Все поля, отмеченные звёздочкой (*), обязательны к заполнению