После нескольких спорных срабатываний команда получает SARIF-файл, видит ruleId, сообщение и позицию в исходнике, но всё равно не может решить, что делать. Одни считают result достаточным основанием для блокировки, другие предлагают убрать правило. Проблема в модели данных: файл результата и правило описывают форму наблюдения, а решение требует ещё контекста компонента и явной политики обработки.
Цена смешения слоёв быстро растёт. Если parser превращает location в доказательство исполнения, создаётся ложная срочность. Если ruleId воспринимается как «шум», правило пропадает для другого участка с иным входом. Если policy скрыта в комментарии к PR, нельзя восстановить владельца, scope и дату пересмотра. Надёжнее разложить сигнал по контракту: формат сохраняет поля, rule задаёт intent, result указывает на совпадение, а человек добавляет проектный контекст и решение.
SARIF — транспорт результата, а не модель приложения
Стандарт SARIF 2.1.0 определяет log с полем version и массивом runs. В run описываются tool, его driver и rules, а results ссылаются на ruleId и locations. Это хороший общий язык между анализатором, CI и просмотрщиком: result не нужно переводить в свободный текст при каждом шаге. Но общность формата имеет границу. SARIF не знает, какой asset важнее, какой feature flag включён, кто подтверждает boundary и какова допустимая политика исключений в этой команде.
Именно поэтому нужно не «обогащать SARIF догадками», а держать рядом отдельную context record. В нашем synthetic примере это пять строк: asset, entry point, trust boundary, owner и release scope. Их можно привязать к result fingerprint, но нельзя выдавать за поля, полученные из scanner. Такой разрыв делает audit честным: можно увидеть, что пришло из инструмента, что добавил reviewer и где решение всё ещё опирается на неизвестное.
| Объект | Пример поля | Кто его создаёт | Что это означает | Чего не доказывает |
|---|---|---|---|---|
| SARIF log | version: 2.1.0 | producer формата | версия транспортного контракта | качество анализа |
| Tool driver | name, version, rules | анализатор | какой набор правил описан | что правило запускалось в вашем CI |
| Rule | id, level, revision | автор policy | intent и конфигурацию правила | что совпадение опасно именно здесь |
| Result | ruleId, message, fingerprint | анализатор | одно совпадение с rule | выполнимость пути или exploitability |
| Location | URI, startLine | анализатор | указатель на позицию | актуальность ревизии или runtime execution |
| Context record | asset, boundary, owner | владелец компонента | условия проектного review | полное покрытие всех сценариев |
Правило должно называть свою гипотезу
Хорошее правило не обязано решать всю задачу безопасности. Ему достаточно точно назвать наблюдаемую гипотезу: «встречена конструкция команды с недоверенным значением». В synthetic contract rule имеет id, revision, category, default level и message. Revision важна: один и тот же id может жить дольше конкретного pattern, а reviewer должен видеть, по какой форме сделано решение. Level задаёт ожидаемую обработку, но не заменяет evidence для одного result.
Ниже показана учебная форма YAML. Она не лежит в файлах проекта и не выполняется fixture. В мае 2023 Semgrep v1.20.0 уже был опубликован, а исходник CLI с этим тегом версии явно объявлял --sarif как output format. Это позволяет сослаться на конкретную версию инструмента и формат, но не позволяет заявить, что правило ниже было запущено Semgrep, увидело исходник или воспроизводит чью-то production policy.
# Учебная форма правила: этот fragment не запускается fixture и не относится к проекту.
rules:
- id: demo.untrusted-command-construction
languages: [javascript]
severity: WARNING
message: "Проверить synthetic построение команды"
patterns:
- pattern: runShell($UNTRUSTED)
В реальном репозитории rule change надо review как код: зафиксировать revision, diff, языки, scope файлов и ожидаемую причину срабатывания. Нельзя оправдать изменение фразой «снизили шум», если не названо, какую форму теперь намеренно игнорируют. Если rule становится уже, это означает управляемый trade-off: часть будущих совпадений больше не придёт. Такой trade-off допустим только рядом с контекстом и владельцем, а не как побочный эффект неясного refactor YAML.
Result и location надо читать как указатель
Result полезен, потому что соединяет message, ruleId и конкретный fingerprint. Fingerprint помогает отличить один результат от соседнего при повторном review. Но fingerprint — это идентификатор сопоставления, а не персональный риск-score. URI и строка тоже не становятся доказательством сами по себе: за время между анализом и review файл мог поменяться, а обработчик мог быть недостижим в интересующем релизе. Поэтому в context record стоит хранить ссылку на ревизию и scope, который проверяется, а рядом писать метод, которым планируется проверить entry point.
В fixture location намеренно синтетическая: src/demo-command.js:14. Assertions проверяют, что строка сохранилась в объекте и что sourceRead равен not-performed-by-fixture. Это защита от удачной, но ложной демонстрации. Если тест мог бы пройти, одновременно делая вывод «строка прочитана анализатором», он учил бы ровно той подмене, против которой направлена статья.
Маршрут: симптом → причина → проверка → действие
- Симптом. У result есть message и location, но reviewer не может объяснить, почему действие должно быть keep, tune или suppress. Не назначайте severity по впечатлению от текста сообщения.
- Причина. Формат обмена, intent правила, совпадение и проектная политика слиты в один JSON или комментарий. Владелец каждого поля исчезает.
- Проверка. Выпишите отдельно version SARIF, tool version, rule id/revision, result fingerprint и location. Затем заведите context record с asset, entry point, boundary, owner и release scope.
- Действие. Свяжите context record с fingerprint, но храните его как проектное evidence. При неполных полях верните статус
context-incomplete, а не подменяйте отсутствующие данные предположением. - Проверка контракта. Запустите fixture. Он должен принять только
synthetic-static-analysis-input-v1, отвергнуть SARIF другой версии и не добавить claims о runtime. - Rollback. Если обсуждение меняет policy, в плане остаётся before state. Реальный rollback требует отдельного config diff и повторного анализа; synthetic output этого не исполняет.
Контекст не заменяет техническую проверку
Context record делает вопрос обозримым, но не превращается в доказательство. Поле entryPoint: demo-http-handler — гипотеза о месте входа. Поле trustBoundary: demo-request-parameter — договорённость, что именно считают недоверенным в этом сценарии. Для реального решения нужно выбрать метод проверки: review data flow, targeted test, trace в разрешённом окружении или другой подход, который команда может повторить. Метод и его границы следует записывать рядом, иначе на следующем review неизвестно, что именно было проверено.
NIST SSDF полезен здесь не как набор волшебных правил, а как напоминание о воспроизводимости безопасной разработки: изменение должно иметь владельца, evidence и путь повторной проверки. Если результат анализатора не совпадает с контекстом компонента, не надо заставлять один из слоёв притворяться другим. Лучше оставить запись «формат и rule известны; контекст или техническая проверка ещё не завершены». Это менее эффектно, чем слово finding, но точнее определяет следующий шаг.
Ограничения модели и следующий шаг
Этот материал не сравнивает анализаторы, не измеряет false positive rate, не утверждает поддержку всех языков и не обещает, что SARIF одинаково заполнен каждым producer. Спецификация SARIF 2.1.0 имеет статус OASIS Standard от 27 марта 2020 года; Semgrep v1.20.0 — выбранная историческая точка от 28 апреля 2023 года. Их наличие не заменяет проверку реальной версии CLI, rule packs, schema validation, base commit и настроек CI конкретного проекта.
Следующий шаг — выбрать один существующий result и собрать два маленьких артефакта: неизменяемую выдержку из SARIF без секретов и context record с пятью полями. Затем на review сформулировать одну гипотезу rule и метод её проверки. Если context не собирается, не переписывайте rule наугад. Сначала назначьте owner и срок. Так формат остаётся транспортом фактов, а решение — проверяемой частью инженерной работы.
Проверяемые источники
- OASIS: Static Analysis Results Interchange Format (SARIF) Version 2.1.0, OASIS Standard, 27 марта 2020 — нормативная спецификация формата результата статического анализа. Она задаёт формат обмена, а не достоверность отдельного result и не решение об исправлении.
- OASIS: SARIF 2.1.0 JSON Schema, 27 марта 2020 — схема формата, доступная вместе со стандартом. Этот fixture собирает учебный объект в памяти и не выполняет валидацию внешним schema validator.
- Semgrep v1.20.0: официальный release, 28 апреля 2023 — версионная точка отсчёта, опубликованная до мая 2023. Она нужна для воспроизводимости, но не говорит, как конкретное правило поведёт себя в вашем репозитории.
- Semgrep v1.20.0: исходник CLI scan.py, опция --sarif — официальный исходник фиксированной версии: CLI объявляет output format SARIF. Он не подтверждает, что этот учебный fixture запускал Semgrep или получил finding из исходного кода.
- NIST SP 800-218, Secure Software Development Framework Version 1.1, Final, 3 февраля 2022 — официальный документ по безопасной разработке, доступный до мая 2023. Здесь он служит рамкой для владения решением и evidence, а не доказательством эффективности правила.