Goast: Общий статический анализ для абстрактного синтаксического дерева Go с помощью OPA/Rego


TL; DR

  • Язык Go имеет различные инструменты статического анализа, но для проверки собственных правил необходимо каждый раз создавать собственные инструменты.
  • Чтобы обеспечить более общие проверки с помощью одного инструмента, я создал инструмент для анализа AST (абстрактного синтаксического дерева) языка Go с помощью Rego, языка политик общего назначения.

https://github.com/m-mizutani/goast


Правило «всегда принимать context.Context в качестве первого аргумента», проверенное CI.

Мотивация

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

  • Все функции должны принимать в качестве аргумента context.Context в некотором пакете
  • Все функции должны вызывать функцию регистрации аудита хотя бы один раз в каком-либо пакете.
  • Структура User должна быть инициализирована функцией NewUser().
  • Некоторая функция должна вызываться только в определенном пакете.

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

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

Разделение «правила» и «реализации» от Rego

Хотя это не только для статического анализа, одним из ключевых моментов в создании универсального инструмента проверки является то, как позволить пользователям описывать правила. Создание оригинального языка описания слишком дорого, а если правила предоставляются в структурных данных, таких как YAML или JSON, выразительные возможности будут ограничены, а универсальность снижена.

Полезным инструментом для таких приложений является язык описания политик Rego. Rego — это язык общего назначения, который можно использовать для оценки структурированных данных с помощью OPA. Некоторые из наиболее популярных применений включают проверку состояния ресурсов, используемых в облачных средах, проверку содержания описаний Infrastructure as Code и проверку авторизации доступа к серверам. Более подробную информацию о Rego можно найти в этом документе.

При использовании Rego реализация проверки и правила могут быть полностью разделены. Реализация отвечает за чтение файлов, чтение политик, передачу данных для оценки и вывод результатов оценки, в то время как правила пишутся только в Rego. Это позволяет разделить интересы между теми, кто реализует инструмент, и теми, кто думает о правилах.

Реализация

Затем я реализовал инструмент общего статического анализа для языка Go, goast.

https://github.com/m-mizutani/goast

Инструмент читает код Go и оценивает AST (Abstruct Syntax Tree, абстрактное дерево синтаксиса), абстрактное представление кода, по политике, написанной на Rego. Пакет parser используется для получения AST исходного кода Go, который затем оценивается политикой Rego. Оценка может передавать AST всего файла только один раз, или предоставлять режим оценки по узлам AST.

Рассмотрим AST кода Go

Я тоже новичок в AST в Go, поэтому не могу представить себе AST, просто глядя на код. Поэтому я добавил в goast функцию для дампа AST для подтверждения.

package main

import "fmt"

func main() {
        fmt.Println("hello")
}
Вход в полноэкранный режим Выход из полноэкранного режима

goast может выводить дамп AST следующей командой.

$ goast dump --line 6  examples/println/main.go | jq
{
  "Path": "examples/println/main.go",
  "Node": {
    "X": {
      "Fun": {
        "X": {
          "NamePos": 44,
          "Name": "fmt",
          "Obj": null
        },
        "Sel": {
          "NamePos": 48,
          "Name": "Println",
          "Obj": null
        }
      },
      "Lparen": 55,
      "Args": [
        {
          "ValuePos": 56,
          "Kind": 9,
          "Value": ""hello""
        }
      ],
      "Ellipsis": 0,
      "Rparen": 63
    }
  },
  "Kind": "ExprStmt"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Структурные данные AST имеют тенденцию быть относительно большими, и даже 7 строк кода, описанные выше, составят 1 408 символов данных JSON. Поэтому для удобства чтения выводится только шестая строка кода (fmt.Println("hello")). Path — путь к прочитанному файлу, Node — дамп ast.Node, переданный ast.Inspect, а Kind — информация о типе Node. s информация о типе.

Как вы можете себе представить, здесь .Node.X.Fun представляет информацию о вызывающей функции, а .Node.X.Args — аргументы. Например, вы можете использовать это для описания правила типа «запретить вызов определенной функции». Вы также можете использовать следующие контексты в качестве дополнительного условия, например.

  • Разрешить/запретить вызовы внутри определенного пакета
  • Разрешить/запретить определенные аргументы
  • Разрешить/запретить прямую передачу литералов

Описать правило

Теперь давайте напишем правила Rego из выведенного AST. На этот раз опишем простое правило, запрещающее вызов fmt.Println.

package goast

fail[res] {
    input.Kind == "ExprStmt"
    input.Node.X.Fun.X.Name == "fmt"
    input.Node.X.Fun.Sel.Name == "Println"

    res := {
        "msg": "do not use fmt.Println",
        "pos": input.Node.X.Fun.X.NamePos,
        "sev": "ERROR",
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Схема правила goast выглядит следующим образом.

  • Ввод: input имеет метаданные, такие какPathKind и Node как фактический AST.
  • Выход: Поместите следующие данные структуры в fail, если обнаружено нарушение

Во-первых, три строки в начале правила обнаруживают fmt.Println, которое было только что выгружено, и поскольку сообщение в только что выгруженном формате передается непосредственно в Rego как input, проверка Kind, Node.X.Fun.X.Name и Node.X.Fun.Sel.Name Name определяет, что это выражение вызова функции.

Если вы не знакомы с AST, вам может быть трудно понять, что означает pos, но в данном случае это число, которое указывает на количество байт от начала файла и хранится в некоторых полях, таких как NamePos и ValuePos. Поместив это число в ответ, goast преобразует его в количество строк в файле, где произошло нарушение, и в конечном выводе будет указано количество строк.

Вы можете обнаружить нарушения, сохранив код Go как main.go, а правило как policy.rego и выполнив команду

$ goast eval -p policy.rego main.go
[main.go:6] - do not use fmt.Println

        Detected 1 violations

Войти в полноэкранный режим Выйти из полноэкранного режима

Кроме того, goast поддерживает вывод данных в формате JSON.

$ goast eval -f json -p policy.rego main.go
{
  "diagnostics": [
    {
      "message": "do not use fmt.Println",
      "location": {
        "path": "main.go",
        "range": {
          "start": {
            "line": 6,
            "column": 2
          }
        }
      }
    }
  ],
  "source": {
    "name": "goast",
    "url": "https://github.com/m-mizutani/goast"
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Статический анализ в CI

Статический анализ должен постоянно выполняться в CI (Continuous Integration) для предотвращения непреднамеренного включения кода. Схема вывода JSON совместима с reviewdog и может использоваться как есть в reviewdog.

У нас также есть goast-action, доступный для использования с GitHub Actions, который позволяет выполнять статическую проверку Pull Requests с помощью следующего рабочего процесса.

name: goast

on:
  pull_request:

jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v2
      - uses: reviewdog/action-setup@v1
      - name: goast
        uses: m-mizutani/goast-action@main
        with:
          policy: ./policy  # Directory of rule files written in Rego
          format: json      # Output format, "text" or "json"
          output: fail.json # File name for output
          source: ./pkg     # Directory of Go source code to be checked
      - name: report
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: cat fail.json | reviewdog -reporter=github-pr-review -f rdjson
Войдите в полноэкранный режим Выйти из полноэкранного режима

В результате этого процесса GitHub Actions отправит комментарий, подобный следующему изображению.

Заключение

Я считаю, что статический анализ с помощью Go AST и Rego позволяет разработчикам более гибко проводить статический анализ для языка Go. Это будет полезно для разработки более безопасного программного обеспечения.

Однако я подумал, что невозможно охватить все статические проверки с помощью AST, который показывает весь исходный код, и универсального языка политик. Например, Rego плохо справляется с написанием правил, отслеживающих изменения состояния, поэтому он не очень подходит для таких случаев использования, как «как переменная ссылается или изменяется».

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

Оцените статью
devanswers.ru
Добавить комментарий