const GROUPS = [
{id:1,name:'Организация и управление',R:.10},
{id:2,name:'Защита пользователей',R:.25},
{id:3,name:'Защита информационных систем',R:.35},
{id:4,name:'Мониторинг информационной безопасности и реагирование',R:.30}
];
const Q = [
{id:'k11',n:1,g:1,k:.30,title:'Возложены ли на одного из заместителей руководителя полномочия ответственного за обеспечение информационной безопасности и определены ли его обязанности?',criterion:'Для положительного ответа должны быть одновременно оформлены назначение заместителя руководителя ответственным за ИБ и его обязанности по обеспечению ИБ.',evidence:'Утверждённый приказ (распоряжение) о назначении + должностной регламент, инструкция или иной документ с обязанностями.',action:'Оформить назначение ответственного заместителя руководителя и закрепить его обязанности',steps:['Подготовить локальный акт о возложении полномочий по обеспечению информационной безопасности.','Закрепить обязанности и ответственность в должностном регламенте, инструкции или ином локальном документе.','Утвердить документы и включить их в комплект подтверждающих материалов.'],tech:false,solutionTypes:[],solutionNote:'Для выполнения этого пункта закупка СЗИ не требуется: необходимы организационно-распорядительные документы.'},
{id:'k12',n:2,g:1,k:.40,title:'Определены и документально закреплены ли функции подразделения или отдельных работников, ответственных за обеспечение информационной безопасности?',criterion:'Функции по ИБ должны быть установлены локальным актом и закреплены за конкретным подразделением или работниками.',evidence:'Положение о подразделении либо иной утверждённый документ о возложении функций и обязанностей по ИБ.',action:'Документально закрепить функции подразделения или работников, ответственных за ИБ',steps:['Определить требуемые функции по информационной безопасности.','Закрепить их в положении о подразделении, должностных документах или отдельном приказе.','Проверить, что обязанности распределены между конкретными работниками и фактически исполняются.'],tech:false,solutionTypes:[],solutionNote:'Закупка СЗИ для выполнения этого пункта не требуется.'},
{id:'k13',n:3,g:1,k:.30,title:'Установлены ли в договорах с подрядчиками, имеющими привилегированный доступ к информационным системам, требования по защите от угроз через инфраструктуру подрядчика?',criterion:'Положительный ответ требует договорного закрепления требований к защите для подрядчиков с привилегированными правами.',evidence:'Договор, контракт, техническое задание или выписка из него с требованиями по защите информации.',exemption:'Не применяется: таких подрядчиков нет',action:'Включить требования ИБ в договоры с подрядчиками, имеющими привилегированный доступ',steps:['Составить перечень подрядчиков с привилегированным доступом.','Добавить в договоры или ТЗ требования по защите от угроз, возникающих через инфраструктуру подрядчика.','Закрепить ответственность и порядок контроля выполнения требований.'],tech:false,solutionTypes:[],solutionNote:'Сам показатель закрывается договорными требованиями. Закупка СЗИ не является самостоятельным условием этого пункта.'},
{id:'k21',n:4,g:2,k:.30,title:'Обеспечено ли отсутствие учётных записей с паролями, не соответствующими установленной парольной политике, либо реализованы подтверждённые компенсирующие меры?',criterion:'Должна действовать утверждённая парольная политика, настройки должны ей соответствовать, а инструментальная проверка не должна выявлять несоответствующие пароли; при технической невозможности нужны компенсирующие меры.',evidence:'ОРД с парольной политикой, настройки механизма её реализации и отчёт инструментальной проверки паролей.',action:'Обеспечить соответствие паролей утверждённой парольной политике',steps:['Проверить и утвердить требования парольной политики.','Настроить их применение в каталогах, доменах, ОС и прикладных системах либо определить компенсирующие меры там, где это технически невозможно.','Провести инструментальную проверку и устранить выявленные несоответствия.'],tech:true,solutionTypes:['средство реализации парольной политики или штатный механизм ОС/каталога','средство анализа защищённости либо инструмент сетевого аудита паролей'],solutionNote:'Отдельное СЗИ для парольной политики может не понадобиться, если функцию обеспечивает существующая ОС или служба каталогов. Для подтверждения результата нужен инструментальный контроль.'},
{id:'k22',n:5,g:2,k:.30,threshold:{pct:50,den:'Всего привилегированных пользователей',num:'Многофакторная аутентификация используется у'},title:'Реализована ли многофакторная аутентификация не менее чем для 50% администраторов, разработчиков и иных привилегированных пользователей?',criterion:'Для положительного ответа второй фактор должен фактически использоваться не менее чем у 50% привилегированных пользователей. Методика отдельно предусматривает специальный случай при отсутствии технической возможности реализации двухфакторной аутентификации.',evidence:'Сведения о применяемом программном или программно-аппаратном средстве и данные о количестве привилегированных пользователей и фактическом охвате многофакторной аутентификацией. Для специального случая необходимо обосновать исходными данными отсутствие технической возможности реализации двухфакторной аутентификации.',exemption:'Специальный случай Методики: техническая возможность реализации двухфакторной аутентификации отсутствует',action:'Обеспечить многофакторную аутентификацию не менее 50% привилегированных пользователей',steps:['Сформировать полный перечень привилегированных пользователей.','Определить системы, где возможно применение второго фактора, и способ его реализации.','Подключить второй фактор как минимум для 50% привилегированных пользователей и подтвердить фактический охват.'],tech:true,solutionTypes:['средство многофакторной аутентификации привилегированных пользователей','модуль многофакторной аутентификации в составе действующей системы управления доступом, IAM/PAM или иного СЗИ'],solutionNote:'Ключевой результат — наличие второго фактора и требуемый охват. Отдельный продукт не обязателен, если нужная функция уже есть в используемой системе.'},
{id:'k23',n:6,g:2,k:.20,title:'Обеспечено ли отсутствие сервисных учётных записей и учётных записей разработчиков с паролями, установленными по умолчанию?',criterion:'Пароли по умолчанию для сервисных и разработческих учётных записей должны быть исключены и это должно подтверждаться настройками и инструментальной проверкой.',evidence:'Настройки сброса/смены пароля после первой аутентификации и отчёт инструментального средства, выявляющего пароли по умолчанию.',action:'Исключить пароли по умолчанию у сервисных и разработческих учётных записей',steps:['Инвентаризировать сервисные и разработческие учётные записи.','Сменить пароли по умолчанию и включить требуемую смену при первичном использовании там, где это применимо.','Провести повторную инструментальную проверку.'],tech:true,solutionTypes:['средство реализации парольной защиты/политики или штатный механизм системы','средство анализа защищённости либо инструмент аудита паролей'],solutionNote:'В большинстве случаев сначала достаточно корректной настройки уже имеющихся систем; отдельная закупка требуется только при отсутствии нужной функции или инструмента контроля.'},
{id:'k24',n:7,g:2,k:.20,title:'Обеспечено ли отсутствие активных учётных записей работников и привлечённых лиц, с которыми прекращены трудовые или иные отношения?',criterion:'Не должно оставаться активных учётных записей бывших работников и подрядчиков; порядок удаления или отключения должен быть установлен организационно.',evidence:'Утверждённый документ о порядке удаления/отключения учётных записей и фактические данные учётных систем.',action:'Отключить неактуальные учётные записи и закрепить процесс при увольнении или завершении договора',steps:['Сверить активные учётные записи со списком действующих работников и подрядчиков.','Отключить или удалить учётные записи лиц, отношения с которыми прекращены.','Закрепить передачу сведений об увольнении/завершении договора администраторам доступа.'],tech:false,solutionTypes:['система управления учётными записями/IAM — как средство автоматизации, но не обязательное условие Методики'],solutionNote:'Показатель можно выполнить организационно и средствами действующих систем. IAM может автоматизировать процесс, но его закупка сама по себе не является обязательным требованием этого пункта.'},
{id:'k31',n:8,g:3,k:.20,threshold:{pct:100,den:'Всего интерфейсов, доступных из сети Интернет',num:'Из них контролируются межсетевыми экранами L3/L4'},title:'Контролируется ли межсетевыми экранами уровня L3/L4 доступ к 100% интерфейсов информационных систем, доступных из сети Интернет?',criterion:'Для положительного ответа межсетевое экранирование уровня L3/L4 должно охватывать все интерфейсы, доступные из сети Интернет.',evidence:'Перечень внешних интерфейсов и сведения/настройки средств межсетевого экранирования с правилами доступа к опубликованным сервисам.',exemption:'Не применяется: интерфейсов и сервисов, доступных из Интернета, нет',action:'Обеспечить межсетевой контроль 100% доступных из Интернета интерфейсов',steps:['Инвентаризировать все публично доступные адреса, интерфейсы и сервисы.','Проверить прохождение трафика через межсетевые экраны уровня L3/L4.','Устранить обходы и подтвердить охват всех внешних интерфейсов.'],tech:true,solutionTypes:['межсетевой экран уровня L3/L4','межсетевой экран нового поколения (NGFW), если он используется как средство межсетевого экранирования и закрывает требуемую функцию'],solutionNote:'Методика требует функцию межсетевого экранирования уровня L3/L4; наличие именно NGFW не является самостоятельным условием.'},
{id:'k32',n:9,g:3,k:.25,title:'Обеспечено ли отсутствие на устройствах и интерфейсах, доступных из сети Интернет, критических уязвимостей, для которых исправления или компенсирующие меры доступны более 30 дней?',criterion:'Критические уязвимости внешнего контура старше 30 дней должны быть устранены либо по ним должны быть реализованы подтверждённые компенсирующие меры.',evidence:'Актуальный отчёт средства анализа защищённости по внешним устройствам и интерфейсам; при необходимости — материалы по компенсирующим мерам.',exemption:'Не применяется: устройств и интерфейсов, доступных из Интернета, нет',action:'Устранить критические уязвимости внешнего контура старше 30 дней',steps:['Провести актуальное сканирование внешних устройств и интерфейсов.','Установить доступные исправления для критических уязвимостей, вышедших за 30-дневный срок.','Где исправление невозможно — реализовать и документировать компенсирующие меры, затем выполнить повторную проверку.'],tech:true,solutionTypes:['средство анализа защищённости (сканер уязвимостей/система управления уязвимостями)','система управления обновлениями — как вспомогательное средство для устранения выявленных уязвимостей'],solutionNote:'Для подтверждения показателя Методика предусматривает отчёт средства анализа защищённости. Средство управления обновлениями может ускорить устранение, но не заменяет подтверждающий анализ.'},
{id:'k33',n:10,g:3,k:.15,threshold:{pct:90,den:'Всего пользовательских устройств и серверов',num:'Из них соответствуют критерию по уязвимостям или компенсирующим мерам'},title:'Обеспечено ли отсутствие критических уязвимостей старше 90 дней не менее чем на 90% пользовательских устройств и серверов либо реализованы подтверждённые компенсирующие меры?',criterion:'Порог охвата — не менее 90% пользовательских устройств и серверов; учитываются критические уязвимости, для которых исправление или компенсирующая мера доступна более 90 дней.',evidence:'Сведения о количестве пользовательских устройств и серверов + отчёт средства анализа защищённости по критическим уязвимостям; при необходимости — материалы по компенсирующим мерам.',action:'Довести не менее 90% пользовательских устройств и серверов до состояния без критических уязвимостей старше 90 дней',steps:['Сформировать полный перечень пользовательских устройств и серверов.','Провести анализ уязвимостей и сформировать очередь критических исправлений.','Установить исправления или компенсирующие меры до достижения порога 90%, затем подтвердить результат повторным анализом.'],tech:true,solutionTypes:['средство анализа защищённости (сканер уязвимостей/система управления уязвимостями)','система управления обновлениями — как вспомогательное средство для массового устранения уязвимостей'],solutionNote:'Ключевое подтверждение — отчёт средства анализа защищённости и достижение порога не менее 90%.'},
{id:'k34',n:11,g:3,k:.15,threshold:{pct:80,den:'Всего пользовательских устройств',num:'Проверка почтовых вложений обеспечена на'},title:'Обеспечена ли проверка вложений электронной почты на вредоносное программное обеспечение не менее чем на 80% пользовательских устройств?',criterion:'Положительный ответ требует фактического охвата проверки почтовых вложений не менее чем на 80% пользовательских устройств.',evidence:'Сведения об общем количестве рабочих мест, применяемом средстве антивирусной защиты и отчёт о количестве устройств, на которых выполняется проверка вложений.',exemption:'Не применяется: электронная почта в оцениваемых системах не используется',action:'Обеспечить проверку почтовых вложений минимум на 80% пользовательских устройств',steps:['Определить фактическое число пользовательских устройств, работающих с электронной почтой.','Включить проверку вложений средствами антивирусной защиты или защитным почтовым шлюзом.','Довести подтверждённый охват минимум до 80% и сформировать отчёт.'],tech:true,solutionTypes:['средство антивирусной защиты с функцией проверки почтовых вложений','защитный почтовый шлюз с антивирусной проверкой вложений'],solutionNote:'Нужна именно функция проверки вложений и подтверждённый охват не менее 80%; она может быть реализована на конечных устройствах либо на почтовом шлюзе.'},
{id:'k35',n:12,g:3,k:.15,threshold:{pct:80,den:'Всего пользовательских устройств и серверов, учитываемых по критерию',num:'Централизованно управляемая антивирусная защита обеспечена на'},title:'Обеспечено ли централизованное управление средствами антивирусной защиты не менее чем на 80% пользовательских устройств и серверов с контролем обновления баз не реже одного раза в месяц?',criterion:'Нужны одновременно требуемый охват, централизованное управление и контроль/установка обновлений баз признаков вредоносного ПО не реже одного раза в месяц.',evidence:'Сведения о количестве устройств, снимок панели централизованного управления и отчёт по обновлениям и проверкам защищаемых объектов. Для применения специального случая — сведения, подтверждающие автономную антивирусную защиту типа «Г» либо конструктивное отсутствие интерфейсов для возможного внедрения вредоносного ПО.',exemptions:['Применяется специальный случай Методики: используемые автономные рабочие места защищены автономными средствами антивирусной защиты типа «Г», а условия показателя для остальных учитываемых устройств выполнены','Применяется специальный случай Методики: устройства, относящиеся к специальному случаю, конструктивно не имеют интерфейсов для возможного внедрения вредоносного ПО, а условия показателя для остальных учитываемых устройств выполнены'],action:'Довести централизованно управляемую антивирусную защиту до требуемого охвата',steps:['Сверить инвентаризацию устройств с консолью централизованного управления.','Подключить отсутствующие устройства либо учесть предусмотренные Методикой специальные случаи.','Настроить контроль обновления баз не реже одного раза в месяц и подтвердить охват отчётом.'],tech:true,solutionTypes:['средство антивирусной защиты с централизованной консолью управления','централизованная система защиты конечных точек, обеспечивающая требуемую антивирусную функцию и отчётность'],solutionNote:'Главное — требуемая антивирусная функция, централизованное управление, охват и подтверждаемое обновление баз.'},
{id:'k36',n:13,g:3,k:.10,title:'Реализована ли очистка входящего из сети Интернет трафика от атак, направленных на отказ в обслуживании, на уровне L3/L4?',criterion:'В типовом случае функция подтверждается договором с провайдером на очистку трафика; допускается собственная реализация, если она подтверждена применяемыми методами и средствами.',evidence:'Договор/выписка с оператором или иной организацией либо сведения о собственной системе защиты от атак на отказ в обслуживании.',exemption:'Не применяется: нет публичных сервисов, подверженных таким атакам',action:'Организовать очистку входящего интернет-трафика от атак на отказ в обслуживании на уровне L3/L4',steps:['Определить публичные сервисы, для которых необходима защита.','Подключить услугу очистки трафика у оператора/специализированной организации либо подтвердить собственный механизм защиты.','Закрепить услугу договором или документировать собственные средства и настройки.'],tech:true,solutionTypes:['услуга защиты от DDoS на уровне L3/L4 у оператора связи или специализированного провайдера','система защиты от DDoS уровня L3/L4 при самостоятельной реализации'],solutionNote:'На практике часто достаточно услуги оператора связи; покупка отдельного аппаратного или программного СЗИ не является обязательной, если требуемая очистка обеспечивается сервисом.'},
{id:'k41',n:14,g:4,k:.40,title:'Реализованы ли централизованный сбор событий безопасности и оповещение о неудачных попытках входа для привилегированных учётных записей?',criterion:'Должен выполняться централизованный сбор соответствующих событий и быть настроено оповещение по неудачным входам для всех учитываемых привилегированных учётных записей.',evidence:'Сведения о привилегированных учётных записях и событиях, настройки системы мониторинга и отчёт о ранее зарегистрированных событиях.',action:'Настроить централизованный сбор событий и оповещение о неудачных входах привилегированных пользователей',steps:['Определить все привилегированные учётные записи и источники соответствующих событий.','Настроить централизованный сбор событий и правила оповещения о неудачных попытках входа.','Проверить срабатывание контрольным событием и сохранить подтверждающий отчёт.'],tech:true,solutionTypes:['система (средство) мониторинга информационной безопасности с централизованным сбором событий и оповещением','система класса SIEM, если она используется для реализации указанной функции'],solutionNote:'Одна система мониторинга может одновременно закрывать этот и следующий пункт при правильном составе источников, правилах анализа и охвате.'},
{id:'k42',n:15,g:4,k:.35,threshold:{pct:100,den:'Всего устройств, взаимодействующих с сетью Интернет',num:'Централизованный сбор и анализ событий настроен на'},title:'Реализованы ли централизованный сбор и анализ событий безопасности на всех устройствах, взаимодействующих с сетью Интернет?',criterion:'Требуется централизованный сбор и анализ событий со всех устройств, взаимодействующих с сетью Интернет; неполный охват не соответствует критерию.',evidence:'Перечень регистрируемых событий и устройств, настройки системы мониторинга и отчёт о зарегистрированных событиях.',action:'Подключить к централизованному мониторингу все устройства, взаимодействующие с сетью Интернет',steps:['Инвентаризировать все устройства, взаимодействующие с сетью Интернет.','Подключить их источники событий к централизованному сбору и настроить анализ.','Сверить список источников с инвентаризацией и подтвердить полный требуемый охват.'],tech:true,solutionTypes:['система (средство) мониторинга информационной безопасности с централизованным сбором и анализом событий','система класса SIEM с подключением всех требуемых источников событий'],solutionNote:'При одновременном невыполнении пунктов 14 и 15 одна корректно спроектированная система мониторинга обычно способна реализовать обе функции; необходимо отдельно обеспечить требуемые источники, правила и охват.'},
{id:'k43',n:16,g:4,k:.25,title:'Утверждён ли организационно-распорядительный документ, определяющий порядок реагирования на компьютерные инциденты?',criterion:'Для положительного ответа требуется утверждённый руководителем или иным уполномоченным лицом документ, устанавливающий порядок реагирования.',evidence:'Утверждённый организационно-распорядительный документ о порядке реагирования на компьютерные инциденты.',action:'Разработать и утвердить порядок реагирования на компьютерные инциденты',steps:['Определить порядок обнаружения, регистрации, эскалации и реагирования на инциденты.','Назначить роли, ответственность и каналы взаимодействия.','Утвердить документ локальным актом и довести его до ответственных лиц.'],tech:false,solutionTypes:['система автоматизации реагирования (SOAR/IRP) — возможна как вспомогательное средство, но не является условием самого показателя'],solutionNote:'Для выполнения показателя нужен утверждённый документ. Покупка отдельной системы автоматизации реагирования не является обязательным условием.'}
];
const SOLUTION_CATALOG = {
k21:{key:'password',name:'Управление парольной политикой и инструментальный контроль паролей',cls:'IAM/IdM + VM',note:'Если парольная политика уже реализуется штатными средствами ОС или каталога, приобретение IAM/IdM не требуется; тогда нужен только инструментальный контроль.'},
k22:{key:'mfa',name:'Многофакторная аутентификация привилегированных пользователей',cls:'MFA / IAM+MFA / PAM+MFA',note:'Знак «/» означает альтернативные варианты реализации. Для выполнения показателя важны второй фактор и требуемый фактический охват.'},
k23:{key:'svcaccounts',name:'Контроль сервисных и разработческих учётных записей',cls:'VM / PAM+VM',note:'PAM или хранилище секретов не обязательны, если смена паролей и контроль уже обеспечиваются штатными средствами; инструментальная проверка остаётся необходимой для подтверждения.'},
k31:{key:'firewall',name:'Межсетевое экранирование внешнего контура',cls:'FW / NGFW',note:'Требуется функция межсетевого экранирования уровня L3/L4. NGFW является возможным классом реализации, но не самостоятельным требованием Методики.'},
k32:{key:'vm',name:'Управление уязвимостями и исправлениями',cls:'VM + Patch Management',note:'VM выявляет и подтверждает состояние уязвимостей; управление обновлениями ускоряет устранение. При наличии действующего механизма обновлений может потребоваться только VM.'},
k33:{key:'vm',name:'Управление уязвимостями и исправлениями',cls:'VM + Patch Management',note:'Одна связка может обслуживать внешний и внутренний контур при достаточном охвате и корректной организации сканирования.'},
k34:{key:'mail',name:'Защита электронной почты и вложений',cls:'SEG / AV-EPP',note:'Это альтернативные способы реализации проверки вложений: на почтовом шлюзе либо на конечных устройствах, если достигается требуемый охват.'},
k35:{key:'endpoint',name:'Централизованная антивирусная защита конечных точек',cls:'AV/EPP / EDR (с антивирусной функцией)',note:'EDR учитывается только если фактически обеспечивает требуемую антивирусную функцию, централизованное управление и отчётность.'},
k36:{key:'ddos',name:'Защита от атак на отказ в обслуживании',cls:'Anti-DDoS / Anti-DDoS Service',note:'Возможна собственная система либо услуга очистки трафика оператора/специализированного провайдера.'},
k41:{key:'siem',name:'Централизованный сбор и оповещение по событиям ИБ',cls:'SIEM',note:'Нужны источники событий по привилегированным учётным записям и правила оповещения о неудачных попытках входа.'},
k42:{key:'siem',name:'Централизованный сбор и анализ событий ИБ',cls:'SIEM',note:'Одна SIEM может одновременно закрывать пункты 14 и 15 при полном требуемом охвате источников и корректных правилах анализа.'}
};
const state = {answers:{}, repeats:{}, implementation:{}, prevAssessment:'', ptNone:false, ptCred:false, ptVuln:false, ptGoal:false, ptUnknown:false, sort:'cheap', rankingDirty:false};
const groupsEl = document.getElementById('groups');
function money(x){ return (Math.round(x*10000)/10000).toFixed(4).replace('.',','); }
function contrib(q){ return q.k * GROUPS.find(g=>g.id===q.g).R; }
function cloneState(){ return {answers:{...state.answers},repeats:{...state.repeats},implementation:{...state.implementation},prevAssessment:state.prevAssessment,ptNone:state.ptNone,ptCred:state.ptCred,ptVuln:state.ptVuln,ptGoal:state.ptGoal,ptUnknown:state.ptUnknown,sort:state.sort,rankingDirty:state.rankingDirty}; }
function calculationFor(st){
const w=Object.fromEntries(GROUPS.map(g=>[g.id,g.R]));
const reasons={};
Q.forEach(q=>{ if((st.answers[q.id]==='no'||st.answers[q.id]==='unknown') && st.repeats[q.id]==='yes'){ w[q.g]=0; (reasons[q.g]??=[]).push(`повторное невыполнение по пункту ${q.n} в течение 12 месяцев`); }});
if(st.ptCred){w[2]=0;(reasons[2]??=[]).push('первоначальный доступ через учётные записи')}
if(st.ptVuln){w[3]=0;(reasons[3]??=[]).push('первоначальный доступ через уязвимость')}
if(st.ptGoal){w[2]=0;w[3]=0;(reasons[2]??=[]).push('подтверждена возможность недопустимого события');(reasons[3]??=[]).push('подтверждена возможность недопустимого события')}
let total=0; const groupRows=[];
GROUPS.forEach(g=>{ const sum=Q.filter(q=>q.g===g.id).reduce((acc,q)=>acc+((st.answers[q.id]==='yes'||String(st.answers[q.id]||'').startsWith('exempt'))?q.k:0),0); const value=sum*w[g.id]; total+=value; groupRows.push({g,sum,weight:w[g.id],value,zero:w[g.id]===0,reasons:reasons[g.id]||[]}); });
return {total,groupRows,w,reasons};
}
function simulateFix(q,baseState){
const st={...baseState,answers:{...baseState.answers,[q.id]:'yes'},repeats:{...baseState.repeats}};
delete st.repeats[q.id];
return {state:st,calc:calculationFor(st)};
}
function renderQuestions(){
groupsEl.innerHTML = GROUPS.map(g=>{
const qs=Q.filter(q=>q.g===g.id);
return `
${opt(q.id,'yes','Да, выполняется')}${opt(q.id,'no','Нет, не выполняется')}${opt(q.id,'unknown','Не могу подтвердить')}${specialOptions}
${renderThresholdHelper(q)}
Пояснение и что понадобится для проверки
Когда можно ответить «Да». ${q.criterion}
Что обычно подтверждает ответ. ${q.evidence}
Результат предыдущей оценки
При предыдущей оценке эта же мера также была невыполнена и показателю было присвоено нулевое значение?
${repOpt(q.id,'yes','Да')}${repOpt(q.id,'no','Нет')}${repOpt(q.id,'unknown','Не могу подтвердить')}
Если текущей оценкой также подтверждено невыполнение этой же меры, повторный 0 в течение 12 месяцев приводит к исключению вклада всей соответствующей группы.
`;
}
function renderThresholdHelper(q){
if(!q.threshold) return '';
return `Не уверены, выполнен ли порог ${q.threshold.pct}%? → Рассчитать
Введите два значения. Помощник проверяет только количественный порог и не заменяет проверку остальных условий пункта.
`;
}
function updateThreshold(id){
const q=Q.find(x=>x.id===id); if(!q||!q.threshold) return;
const denEl=document.querySelector(`input[data-threshold-input="${id}"][data-threshold-role="den"]`);
const numEl=document.querySelector(`input[data-threshold-input="${id}"][data-threshold-role="num"]`);
const res=document.getElementById(`thr-${id}`); if(!denEl||!numEl||!res) return;
const den=Number(denEl.value), num=Number(numEl.value);
res.className='threshold-result';
if(denEl.value===''||numEl.value===''){res.textContent='Введите два значения. Помощник проверяет только количественный порог и не заменяет проверку остальных условий пункта.';return;}
if(!Number.isFinite(den)||!Number.isFinite(num)||den<=0||num<0||num>den){res.classList.add('invalid');res.textContent='Проверьте значения: общее количество должно быть больше 0, а охваченное количество — от 0 до общего.';return;}
const pct=num/den*100; const pass=pct+1e-9>=q.threshold.pct;
res.classList.add(pass?'pass':'fail');
res.textContent=`${num}/${den} = ${pct.toFixed(1).replace('.',',')}% → количественный порог ${q.threshold.pct}% ${pass?'выполнен':'не выполнен'}. Это не подтверждает остальные условия показателя.`;
}
function syncRepeatVisibility(id){
const rep=document.getElementById(`rep-${id}`); if(!rep) return;
const show=state.prevAssessment==='yes' && (state.answers[id]==='no'||state.answers[id]==='unknown');
rep.classList.toggle('show',show);
if(!show){delete state.repeats[id];document.querySelectorAll(`input[name="r-${id}"]`).forEach(x=>x.checked=false);}
}
function syncAllRepeats(){ Q.forEach(q=>syncRepeatVisibility(q.id)); }
function opt(id,val,label){return ``}
function repOpt(id,val,label){return ``}
function calculation(){ return calculationFor(state); }
function conditionsAccepted(){
return document.getElementById('scopeAll').checked && document.getElementById('scopeWorst').checked;
}
function applyAcceptanceGate(){
const accepted=conditionsAccepted();
const ids=['ptNone','ptCred','ptVuln','ptGoal','ptUnknown'];
ids.forEach(id=>{const el=document.getElementById(id); if(el) el.disabled=!accepted;});
document.querySelectorAll('input[name="prevAssessment"], #groups input').forEach(el=>el.disabled=!accepted);
['specialSection','prevSection','groups'].forEach(id=>{const el=document.getElementById(id); if(el) el.classList.toggle('gate-locked',!accepted);});
const st=document.getElementById('acceptStatus');
if(st){
st.className='accept-status '+(accepted?'ok':'pending');
st.textContent=accepted?'Условия приняты. Можно переходить к тестированию.':'Для продолжения установите обе обязательные отметки.';
}
const downloadBtn=document.getElementById('downloadBtn');
if(downloadBtn){
downloadBtn.disabled=!accepted;
downloadBtn.title=accepted?'Скачать текстовый результат':'Скачивание станет доступно после принятия обязательных условий заполнения';
}
return accepted;
}
function section2Answered(){
return state.ptNone || state.ptCred || state.ptVuln || state.ptGoal || state.ptUnknown;
}
function completionStatus(){
const answered=Q.filter(q=>state.answers[q.id]).length;
const section2=section2Answered();
const previous=Boolean(state.prevAssessment);
const requiredRepeats=state.prevAssessment==='yes'
? Q.filter(q=>state.answers[q.id]==='no'||state.answers[q.id]==='unknown')
: [];
const missingRepeats=requiredRepeats.filter(q=>!state.repeats[q.id]);
const complete=answered===Q.length && section2 && previous && missingRepeats.length===0;
return {complete,answered,section2,previous,requiredRepeats,missingRepeats};
}
function completionMissingText(c){
const parts=[];
if(c.answered(state.answers[q.id]==='no'||state.answers[q.id]==='unknown') && state.repeats[q.id]==='unknown');
}
return false;
}
function setRankActionVisible(visible){
const action=document.getElementById('rankAction');
if(action) action.classList.toggle('hidden',!visible);
}
function setPlanControlsVisible(visible){
const controls=document.getElementById('planControls');
if(controls) controls.classList.toggle('hidden',!visible);
}
function update(){
const accepted=applyAcceptanceGate();
const prev=document.querySelector('input[name="prevAssessment"]:checked'); state.prevAssessment=prev?prev.value:'';
state.ptNone=document.getElementById('ptNone').checked;state.ptCred=document.getElementById('ptCred').checked;state.ptVuln=document.getElementById('ptVuln').checked;state.ptGoal=document.getElementById('ptGoal').checked;state.ptUnknown=document.getElementById('ptUnknown').checked;
const answered=Q.filter(q=>state.answers[q.id]).length; const pct=answered/Q.length*100;
if(!accepted){
document.getElementById('progressbar').style.width='0%';
document.getElementById('progresstext').textContent='Тестирование доступно после принятия обязательных условий';
document.getElementById('score').textContent='—';
const st=document.getElementById('state'); st.className='state pending'; st.textContent='Сначала примите условия заполнения';
document.getElementById('breakdown').innerHTML='';
document.getElementById('alerts').innerHTML='
Тестирование пока недоступно. Прочитайте условия в пункте 1 и установите обе обязательные галочки.
${r.g.name}Выполнено внутри группы: ${Math.round(r.sum*100)}% · ${r.zero?'вклад группы исключён специальным правилом':'максимальный вклад '+r.g.R.toFixed(2).replace('.',',')}
${money(r.value)}
`).join('');
}
function renderAlerts(calc,answered){
const a=[];
const unknownNow=Q.filter(q=>state.answers[q.id]==='unknown');
if(unknownNow.length) a.push(`
Консервативный принцип расчёта: ${unknownNow.length} ответ(а/ов) «Не могу подтвердить» получили нулевое значение. Такой подход предотвращает завышение Кзи; при уточнении исходных данных результат может измениться.
Заполните пункт 2: укажите, что такие проверки не проводились, либо отметьте известный результат проведённых проверок/учений.
`);
if(state.ptUnknown) a.push(`
Неизвестен результат тестирования на проникновение/учений. Специальные условия Методики сейчас не применены автоматически, поэтому итог следует считать ориентировочным.
`);
if(!state.prevAssessment) a.push(`
Укажите в пункте 3, проводилась ли предыдущая оценка Кзи в течение последних 12 месяцев.
`);
if(state.prevAssessment==='unknown') a.push(`
Неизвестно, проводилась ли предыдущая оценка Кзи в течение последних 12 месяцев. Правило повторного невыполнения меры не применено автоматически; итог может потребовать уточнения.
Консервативное применение правила 12 месяцев. По ${conservativeRepeats.map(q=>'пункту '+q.n).join(', ')} текущее выполнение не подтверждено, а на прошлой оценке был 0. Соответствующая группа обнулена в рамках предварительного консервативного расчёта. Для формальной оценки необходимо установить факт повторного невыполнения меры.
По ${unknownPrev.map(q=>'пункту '+q.n).join(', ')} не удалось подтвердить результат предыдущей оценки. Дополнительное обнуление группы автоматически не применено.
Промежуточный Кзи — расчёт не завершён. Число уже пересчитывается по введённым сведениям, но окончательная цветовая классификация не применяется, пока не заполнены все обязательные сведения${missing?`: ${missing}`:''}.
Пункт ${x.q.n}: ${shortTitle(x.q.title)}до −${money(x.loss)}
${x.unknown?'Выполнение меры не подтверждено имеющимися сведениями — в консервативном расчёте показатель равен 0. Сначала проверьте фактическое состояние.':'По вашему ответу мера сейчас не выполнена — в расчёте показатель равен 0.'} ${state.repeats[x.q.id]==='yes'?(x.unknown?'В рамках консервативного расчёта предыдущий 0 также исключает вклад всей группы; для формальной оценки необходимо установить факт повторного невыполнения меры.':'Повторное невыполнение в течение 12 месяцев исключает вклад всей этой группы.') :''}
`).join('');
}
function shortTitle(s){return s.length>90?s.slice(0,87)+'…':s}
function specialBlockers(calc){
const out=[];
if(state.ptGoal) out.push({groups:[2,3],text:'Подтверждена возможность реализации недопустимого события: вклад групп «Защита пользователей» и «Защита информационных систем» исключён из расчёта. Устраните причины достигнутой цели атаки и подтвердите результат новой проверкой.'});
else {
if(state.ptCred) out.push({groups:[2],text:'Получен первоначальный доступ через учётные записи: вклад группы «Защита пользователей» исключён из расчёта. Требуется устранить причину успешного доступа и подтвердить результат последующей проверкой.'});
if(state.ptVuln) out.push({groups:[3],text:'Получен первоначальный доступ через уязвимость: вклад группы «Защита информационных систем» исключён из расчёта. Требуется устранить уязвимость или условия её эксплуатации и подтвердить результат последующей проверкой.'});
}
Object.entries(calc.reasons).forEach(([g,rs])=>{
const repeat=rs.filter(x=>x.startsWith('повторное'));
if(repeat.length){ const cq=Q.filter(q=>q.g===Number(g) && state.answers[q.id]==='unknown' && state.repeats[q.id]==='yes'); const caveat=cq.length?' Группа обнулена в рамках консервативного предварительного расчёта; для формальной оценки необходимо установить факт повторного невыполнения меры.':''; out.push({groups:[Number(g)],text:`Вклад группы «${GROUPS.find(x=>x.id===Number(g)).name}» исключён из расчёта из-за повторного нулевого значения в пределах 12 месяцев (${repeat.join('; ')}).${caveat}`}); }
});
return out;
}
function buildPlanItems(){
const base=calculation();
return Q.filter(q=>state.answers[q.id]==='no'||state.answers[q.id]==='unknown').map(q=>{
const sim=simulateFix(q,state);
const actual=Math.max(0,sim.calc.total-base.total);
const nominal=contrib(q);
const kind=state.answers[q.id]==='unknown'?'verify':'gap';
return {q,kind,actual,nominal,blocked:actual<1e-9 && nominal>0 && base.w[q.g]===0,after:sim.calc.total};
});
}
function practicalRank(item){
if(item.kind==='verify') return 0;
if(!item.q.tech) return 1;
const impl=state.implementation[item.q.id]||'unknown';
return ({exists:2,unknown:3,partial:4,absent:5})[impl] ?? 3;
}
function sortPlan(items){
const arr=[...items];
if(state.sort==='gain') return arr.sort((a,b)=>(b.actual-a.actual)||(b.nominal-a.nominal)||(practicalRank(a)-practicalRank(b)));
if(state.sort==='balanced') return arr.sort((a,b)=>{
const ae=(a.actual||a.nominal)/(practicalRank(a)+1), be=(b.actual||b.nominal)/(practicalRank(b)+1);
return (be-ae)||(practicalRank(a)-practicalRank(b))||(b.nominal-a.nominal);
});
return arr.sort((a,b)=>(practicalRank(a)-practicalRank(b))||(b.actual-a.actual)||(b.nominal-a.nominal));
}
function displayedPlanOrder(items){
const ids=[...document.querySelectorAll('.roadstep[data-qid]')].map(el=>el.dataset.qid);
if(!ids.length) return [...items];
const byId=new Map(items.map(x=>[x.q.id,x]));
const out=[];
ids.forEach(id=>{if(byId.has(id)){out.push(byId.get(id));byId.delete(id);}});
return out.concat([...byId.values()]);
}
function implementationTypeLabel(q){
if(!q.tech) return 'организационная мера';
return ({exists:'настройка существующего средства',partial:'расширение существующего средства',absent:'может потребоваться приобретение / сервис',unknown:'сначала уточнить наличие средства'})[state.implementation[q.id]||'unknown'];
}
function updateRankControls(){
document.querySelectorAll('.sortbtn[data-sort]').forEach(btn=>btn.classList.toggle('active',btn.dataset.sort===state.sort));
const action=document.getElementById('rankAction'), status=document.getElementById('rankStatus'), btn=document.getElementById('rankPlanBtn');
if(action) action.classList.toggle('dirty',Boolean(state.rankingDirty));
if(status) status.textContent=state.rankingDirty
? 'Сведения или принцип ранжирования изменены. Текущий порядок карточек сохранён; примените ранжирование после заполнения.'
: 'Порядок плана соответствует выбранному принципу ранжирования.';
if(btn) btn.textContent=state.rankingDirty?'Ранжировать план — применить изменения':'Ранжировать план';
}
function refreshImplementationCard(qid){
const q=Q.find(x=>x.id===qid); if(!q) return;
const block=document.querySelector(`.implblock[data-impl-block="${qid}"]`);
if(block) block.outerHTML=implementationSelect(q);
const type=document.querySelector(`[data-impl-type-label="${qid}"]`);
if(type) type.textContent=implementationTypeLabel(q);
bindPlanControls();
renderProcurement();
updateRankControls();
}
function solutionRecommendation(q){
const v=state.implementation[q.id]||'unknown';
const types=(q.solutionTypes||[]).map(t=>`
${t}
`).join('');
const cat=SOLUTION_CATALOG[q.id];
let cls='unknown', lead='Сначала уточните, имеется ли в действующей инфраструктуре требуемая функция.';
if(v==='exists'){cls='exists';lead='Новое средство покупать автоматически не требуется. Сначала проверьте, обеспечивает ли существующее решение весь критерий и требуемый охват.'}
if(v==='partial'){cls='expand';lead='Имеющаяся функция реализована частично. Рассматривайте расширение текущего решения либо добавление недостающего функционального типа.'}
if(v==='absent'){cls='buy';lead='Требуемой функции нет. Для реализации меры следует рассмотреть приобретение или подключение решения соответствующего функционального типа.'}
const classLine=cat?`
Тип решения (без вендора)${lead}${classLine}${types?`
${types}
`:''}${noteText?`${noteText}`:''}
`;
}
function implementationSelect(q){
if(!q.tech) return q.solutionNote?`
Необходимость технического средства${q.solutionNote}
`:'';
const v=state.implementation[q.id]||'unknown';
const labels={
unknown:'Неизвестно — сначала уточнить состояние',
exists:'Средство есть — требуется настройка или подтверждение',
partial:'Средство/функция есть частично — требуется расширение',
absent:'Средства/функции нет — требуется приобретение или сервис'
};
const choices=Object.entries(labels).map(([value,label])=>``).join('');
return `
Наличие средства/функции:
${choices}
Выбрано: ${labels[v]}
${solutionRecommendation(q)}
`;
}
function procurementItems(){
const grouped=new Map();
Q.forEach(q=>{
const ans=state.answers[q.id];
if(ans!=='no' || !q.tech) return;
const impl=state.implementation[q.id]||'unknown';
const cat=SOLUTION_CATALOG[q.id];
if(!cat) return;
if(!grouped.has(cat.key)) grouped.set(cat.key,{...cat,points:[],statuses:[]});
const item=grouped.get(cat.key);
item.points.push(q.n);
item.statuses.push(impl);
});
return [...grouped.values()].map(x=>{
let status='unknown';
if(x.statuses.includes('absent')) status='buy';
else if(x.statuses.includes('partial')) status='expand';
else if(x.statuses.includes('unknown')) status='unknown';
else if(x.statuses.includes('exists')) status='configure';
return {...x,status,points:[...new Set(x.points)].sort((a,b)=>a-b)};
});
}
function procurementBundle(items){
const keys=new Set(items.map(x=>x.key));
const bundles=[];
if(keys.has('vm') && keys.has('siem')) bundles.push({cls:'VM + SIEM',text:'базовая связка управления уязвимостями и централизованного мониторинга',keys:['vm','siem']});
if(keys.has('firewall') && keys.has('ddos')) bundles.push({cls:'FW/NGFW + Anti-DDoS',text:'периметровая связка межсетевого контроля и очистки входящего трафика',keys:['firewall','ddos']});
if(keys.has('mail') && keys.has('endpoint')) bundles.push({cls:'SEG + AV/EPP',text:'связка защиты почтового канала и конечных точек',keys:['mail','endpoint']});
return bundles;
}
function renderProcurement(){
const box=document.getElementById('procurement');
if(!box) return;
const completion=completionStatus();
if(!completion.complete){box.innerHTML='';return;}
const items=procurementItems();
if(!items.length){box.innerHTML='
Возможные функциональные средства для закрытия выявленных разрывов
По подтверждённым ответам «Нет, не выполняется» отдельные технические средства не определены. Пункты «Не могу подтвердить» сначала требуют проверки фактического состояния.
Возможные функциональные средства для закрытия выявленных разрывов
Этот блок формируется только по пунктам, где выбрано «Нет, не выполняется». Ответ «Не могу подтвердить» сначала требует проверки фактического состояния. Функциональный тип указан по-русски, в скобках — рыночный класс решения. «+» означает совместное применение классов, «/» — альтернативные варианты реализации.
До оранжевого уровня${calc.total>.75?'уже достигнут':orangeAt?orangeAt+' шаг.':'не достигается только частными мерами'}Порог: Кзи > 0,75
Потенциал после проверки/устранения всех пунктов${money(final)}${Math.abs(final-1)<1e-9?'нормированное значение Кзи = 1':'учтите блокирующие условия'}
`;
roadmap.innerHTML=rows;
setPlanControlsVisible(true);
setRankActionVisible(true);
bindPlanControls();
renderProcurement();
}
function bindPlanControls(){
document.querySelectorAll('.sortbtn[data-sort]').forEach(btn=>{
if(!btn.dataset.bound){
btn.dataset.bound='1';
btn.addEventListener('click',()=>{
state.sort=btn.dataset.sort;
state.rankingDirty=true;
updateRankControls();
});
}
});
const rankBtn=document.getElementById('rankPlanBtn');
if(rankBtn && !rankBtn.dataset.bound){
rankBtn.dataset.bound='1';
rankBtn.addEventListener('click',()=>{
state.rankingDirty=false;
renderPlan(calculation(),Q.filter(q=>state.answers[q.id]).length);
requestAnimationFrame(()=>{
const action=document.getElementById('rankAction');
if(action) action.scrollIntoView({block:'nearest',behavior:'smooth'});
});
});
}
document.querySelectorAll('button[data-impl-choice]').forEach(btn=>{
if(btn.dataset.bound) return; btn.dataset.bound='1';
btn.addEventListener('click',()=>{
const qid=btn.dataset.implChoice;
state.implementation[qid]=btn.dataset.value;
state.rankingDirty=true;
refreshImplementationCard(qid);
requestAnimationFrame(()=>{
const step=document.querySelector(`.roadstep[data-qid="${qid}"]`);
if(step){
step.classList.add('selection-confirmed');
setTimeout(()=>step.classList.remove('selection-confirmed'),1200);
}
});
});
});
updateRankControls();
}
function selectedAnswerLabel(q,a){
if(a==='yes') return 'да, выполняется';
if(a==='no') return 'нет, не выполняется';
if(a==='unknown') return 'не могу подтвердить — в консервативном предварительном расчёте присвоено 0';
if(String(a||'').startsWith('exempt')){
if(q.exemptions){const i=Number(String(a).replace('exempt','')); return q.exemptions[i]||'специальный случай Методики';}
return q.exemption||'специальный случай Методики';
}
return 'не отвечено';
}
function summaryText(){
const calc=calculation(), answered=Q.filter(q=>state.answers[q.id]).length; const lines=[];
const objectName=(document.getElementById('objectName').value||'').trim();
const today=new Date(); const date=today.toLocaleDateString('ru-RU');
lines.push('Самооценка показателя защищённости Кзи'); lines.push('Методика ФСТЭК России от 11.11.2025'); lines.push(`Дата расчёта: ${date}`); if(objectName) lines.push(`Организация / объект оценки: ${objectName}`); lines.push('');
if(!conditionsAccepted()){
lines.push('Кзи: не рассчитан — условия заполнения не приняты.');
lines.push('Для начала тестирования необходимо прочитать и принять оба обязательных условия в пункте 1.');
lines.push('');
lines.push('Расчёт выполняется исключительно на основании введённых пользователем сведений. Сервис не проверяет их полноту, актуальность и достоверность.');
return lines.join('\n');
}
const completion=completionStatus();
lines.push(`Заполнено показателей: ${answered}/${Q.length}`); lines.push(`Кзи: ${money(calc.total)}${completion.complete?'':' (промежуточный расчёт — не завершён)'}`);
lines.push('Результаты проверок защищённости, тестирования, учений и тренировок:');
if(state.ptNone) lines.push('- такие мероприятия не проводились.');
if(state.ptUnknown) lines.push('- результат неизвестен; специальные условия не применялись автоматически.');
if(state.ptCred){
lines.push('- получен первоначальный доступ к информационной системе с использованием учётной записи пользователя.');
lines.push(' Следствие: вклад группы «Защита пользователей» исключён из расчёта.');
}
if(state.ptVuln){
lines.push('- получен первоначальный доступ с использованием уязвимости программного или программно-аппаратного обеспечения.');
lines.push(' Следствие: вклад группы «Защита информационных систем» исключён из расчёта.');
}
if(state.ptGoal){
lines.push('- достигнуты цели испытания и подтверждена возможность реализации недопустимого события.');
lines.push(' Следствие: вклад групп «Защита пользователей» и «Защита информационных систем» исключён из расчёта.');
}
if(!section2Answered()) lines.push('- не заполнено.');
lines.push(`Предыдущая оценка Кзи в течение 12 месяцев: ${{yes:'проводилась',no:'не проводилась',unknown:'неизвестно'}[state.prevAssessment]||'не указано'}.`); if(completion.complete) { lines.push(`Состояние: ${zone(calc.total).label}`); if(hasUnresolvedSpecialConditions()) lines.push('Статус: есть неуточнённые специальные условия; итоговая классификация рассчитана по введённым сведениям и может потребовать уточнения.'); } else lines.push(`Для завершения расчёта необходимо: ${completionMissingText(completion)||'заполнить обязательные сведения'}.`); lines.push('');
calc.groupRows.forEach(r=>lines.push(`${r.g.name}: выполнено ${Math.round(r.sum*100)}%; вклад=${money(r.value)}${r.zero?' [ВКЛАД ГРУППЫ ИСКЛЮЧЁН СПЕЦИАЛЬНЫМ ПРАВИЛОМ]':''}`));
lines.push('');
Q.forEach(q=>{const a=state.answers[q.id]||''; let extra=''; if(state.prevAssessment==='yes'&&(a==='no'||a==='unknown')) extra=state.repeats[q.id]?`; результат этой меры на предыдущей оценке: ${{yes:'также 0',no:'не был 0',unknown:'не могу подтвердить'}[state.repeats[q.id]]}`:'; результат этой меры на предыдущей оценке: не указан'; lines.push(`Пункт ${q.n}: ${selectedAnswerLabel(q,a)}${extra}`)});
if(completion.complete){
const plan=displayedPlanOrder(buildPlanItems());
if(plan.length){ lines.push(''); lines.push('Практический план повышения Кзи:'); plan.forEach((x,i)=>{ if(x.kind==='verify'){ lines.push(`${i+1}. Проверить фактическое состояние по пункту ${x.q.n}; потенциальный прирост при подтверждении выполнения — до +${money(x.nominal)}. Подтверждение: ${x.q.evidence}`); return; } let text=`${i+1}. ${x.q.action}; вклад при выполнении до +${money(x.nominal)}`; const impl=state.implementation[x.q.id]||'unknown'; const cat=SOLUTION_CATALOG[x.q.id]; if(x.q.tech) { text+=`; состояние средства: ${{exists:'есть — требуется настройка/проверка охвата',partial:'есть частично — проверить расширение',absent:'нет — рассмотреть приобретение/подключение',unknown:'неизвестно — сначала уточнить'}[impl]}`; if(impl!=='exists' && cat) text+=`; возможный класс: ${cat.cls}`; } lines.push(text);}); lines.push('Срок мероприятий при низком или критическом уровне: определить в плане, но не позднее следующей плановой оценки Кзи.'); }
const procurement=procurementItems();
if(procurement.length){ lines.push(''); lines.push('Возможные функциональные средства для закрытия выявленных разрывов:'); procurement.forEach((x,i)=>lines.push(`${i+1}. ${x.name} (${x.cls}) — пункты ${x.points.join(', ')}; ${{buy:'рассмотреть приобретение/подключение',expand:'проверить возможность расширения',unknown:'сначала уточнить состояние',configure:'настройка/проверка охвата существующего средства'}[x.status]}`)); const bundles=procurementBundle(procurement); if(bundles.length){lines.push('');lines.push('Типовые комбинации по совокупности потребностей:');bundles.forEach(b=>lines.push(`${b.cls} — ${b.text}`));}}
} else {
lines.push('');
lines.push('Практический план и перечень возможных функциональных средств не сформированы: сначала завершите обязательные разделы расчёта.');
}
lines.push(''); lines.push('Консервативный принцип расчёта: если выполнение меры невозможно подтвердить имеющимися сведениями, для предварительной оценки ей присваивается 0. Такой подход предотвращает завышение Кзи; при уточнении исходных данных результат может измениться.'); lines.push('Расчёт выполняется исключительно на основании введённых пользователем сведений. Сервис не проверяет их полноту, актуальность и достоверность. Результат носит справочный характер и не заменяет официальную оценку с анализом подтверждающих материалов.');
return lines.join('\n');
}
['ptCred','ptVuln','ptGoal'].forEach(id=>document.getElementById(id).addEventListener('change',e=>{if(e.target.checked){document.getElementById('ptUnknown').checked=false;document.getElementById('ptNone').checked=false;}update();}));
document.getElementById('ptUnknown').addEventListener('change',e=>{if(e.target.checked){['ptCred','ptVuln','ptGoal'].forEach(id=>document.getElementById(id).checked=false);document.getElementById('ptNone').checked=false;}update();});
document.getElementById('ptNone').addEventListener('change',e=>{if(e.target.checked)['ptCred','ptVuln','ptGoal','ptUnknown'].forEach(id=>document.getElementById(id).checked=false);update();});
document.getElementById('scopeAll').addEventListener('change',update);document.getElementById('scopeWorst').addEventListener('change',update);
document.querySelectorAll('input[name="prevAssessment"]').forEach(el=>el.addEventListener('change',e=>{state.prevAssessment=e.target.value;if(state.prevAssessment!=='yes'){state.repeats={};document.querySelectorAll('input[data-repeat]').forEach(x=>x.checked=false);}syncAllRepeats();update();}));
document.getElementById('printBtn').addEventListener('click',()=>window.print());
document.getElementById('copyBtn').addEventListener('click',async()=>{try{await navigator.clipboard.writeText(summaryText());const b=document.getElementById('copyBtn');const old=b.textContent;b.textContent='Скопировано';setTimeout(()=>b.textContent=old,1200)}catch(e){alert('Не удалось скопировать автоматически. Используйте печать или скачивание результата.')}});
document.getElementById('downloadBtn').addEventListener('click',()=>{const blob=new Blob([summaryText()],{type:'text/plain;charset=utf-8'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);const d=new Date();const ds=[d.getFullYear(),String(d.getMonth()+1).padStart(2,'0'),String(d.getDate()).padStart(2,'0')].join('-');a.download=`kzi-result-${ds}.txt`;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),500)});
document.getElementById('resetBtn').addEventListener('click',()=>{if(!confirm('Сбросить все ответы?'))return;state.answers={};state.repeats={};state.implementation={};state.prevAssessment='';state.sort='cheap';state.rankingDirty=false;document.querySelectorAll('input').forEach(x=>{if(x.type==='radio'||x.type==='checkbox')x.checked=false;else x.value=''});document.querySelectorAll('.repeat').forEach(x=>x.classList.remove('show'));update();window.scrollTo({top:0,behavior:'smooth'})});
renderQuestions(); update();