8000 GitHub - qwexvf/aegis.nvim: Supply-chain gate for lazy.nvim — gates clone, update, build and load on an aegis-cli verdict for that exact commit · GitHub
[go: up one dir, main page]

Skip to content
  • About

    Supply-chain gate for lazy.nvim — gates clone, update, build and load on an aegis-cli verdict for that exact commit

    Resources

    Stars

    2 stars

    Watchers

    0 watching

    Forks

    Latest commit

     

    History

    10 Commits

    Folders and files

    Repository files navigation

    aegis.nvim

    Supply-chain gate for lazy.nvim, backed by aegis-cli.

    Every point where a plugin repository can get its own code executed is gated on an aegis verdict for that exact commit. A plugin that fails the policy doesn't get a warning after the fact. It never gets cloned, checked out, built, or added to runtimepath.

    :Lazy install                        :Lazy update
      git.clone                            git.fetch          objects only, worktree untouched
      └─ AEGIS scan ───── block ─┐         git.checkout
      git.checkout               │         └─ AEGIS scan target ── block ─┐   worktree never moves
      plugin.docs                │         plugin.docs                    │
      plugin.build               │         plugin.build                   │
      └─ AEGIS re-check ── block ┤         └─ AEGIS re-check ──── block ───┤
                                 │                                        │
    startup                      │                                        │
      defaults.cond              │                                        │
      └─ AEGIS cached verdict ── block ─── plugin never reaches runtimepath ┘
    

    Why the gates sit where they do

    Cloning a repository does not execute anything. Five things do, and each has a gate or is switched off outright:

    Execution path When Handled by
    plugin.build (shell strings, build.lua, :cmd) after install/update build gate
    plugin/, ftdetect/, after/plugin/ auto-sourcing when lazy adds the dir to runtimepath load gate (defaults.cond)
    lazy reading the repo's own lazy.lua / rockspec / packspec to build a spec during spec resolve, before install pkg.enabled = false
    luarocks build scripts install rocks.enabled = false
    submodule URLs (ext:: RCE on older git) fetched during clone defaults.submodules = false

    The last three are not gated, they are removed. They run repo-authored code earlier than any scan could see it, so there is nowhere to put a check.

    Requirements

    • Neovim 0.10+
    • lazy.nvim
    • aegis on PATH (aegis analyze --ecosystem neovim must work)

    Install

    aegis.nvim has to be on runtimepath before lazy.setup() runs, so it is bootstrapped next to lazy itself rather than managed by it.

    local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
    local aegispath = vim.fn.stdpath("data") .. "/lazy/aegis.nvim"
    
    for path, url in pairs({
      [lazypath] = "https://github.com/folke/lazy.nvim.git",
      [aegispath] = "https://github.com/qwexvf/aegis.nvim.git",
    }) do
      if not vim.uv.fs_stat(path) then
        vim.fn.system({ "git", "clone", "--filter=blob:none", url, path })
      end
    end
    vim.opt.rtp:prepend(aegispath)
    vim.opt.rtp:prepend(lazypath)
    
    require("aegis").setup({})
    
    require("lazy").setup(spec, require("aegis").lazy_opts({
      -- your usual lazy options
    }))

    lazy_opts() is what wires everything in. You have to pass your options through it. Without it there is no hardening and no load gate, and :checkhealth aegis will tell you.

    To let :Lazy update keep aegis.nvim itself current, add a matching spec that points at the bootstrap clone:

    { "qwexvf/aegis.nvim", dir = aegispath },

    Configuration

    Defaults:

    require("aegis").setup({
      bin = "aegis",
      fail_on = "prompt",          -- safe | review | prompt | block
    
      gates = {
        clone = true,
        update = true,
        build = true,
        load = true,
      },
    
      strict_clone = false,        -- scan in a throwaway clone first
    
      harden = {
        pkg = true,
        rocks = true,
        submodules = true,
        require_https = true,
        allowed_hosts = {},        -- e.g. { "github.com" }
      },
    
      on_missing_scanner = "warn", -- "warn" | "block"
      on_scan_error = "block",     -- "allow" | "block"
      load_unknown = "allow",      -- "allow" | "block"
    
      evidence = true,
      timeout = 120000,
      notify = true,
      state_dir = nil,             -- default stdpath("state")/aegis-nvim
    })

    fail_on defaults to prompt, not block

    aegis scores a Lua tree from the capabilities it finds. As of aegis 0.32: shell-spawn 10, dynamic-eval 15, fs-write-outside-root 8, raw-ip-literal 10, a credential-looking environment read 15, loading native code (ffi.load, package.cpath, package.loadlib) 20. Verdict tiers are review ≥21, prompt ≥61, block ≥100. aegis.nvim needs 0.32 or later; on 0.30 and 0.31 Lua credential reads and native loading score nothing, and the test suite's malicious fixture slips through at review.

    For a Lua-only plugin the block tier is realistically only reached via a malicious build hook (install-hook-suspicious, weight 70). Gating on block would therefore let the interesting cases through, so the default is prompt: aegis's own word for "a human has to say yes". :Aegis approve is that yes.

    The test suite's malicious fixture shells out, evals a downloaded payload, reads GITHUB_TOKEN, writes outside its directory, talks to a raw IP and loads a native library. It scores 78: prompt, blocked by the default.

    Measured with aegis 0.32.0:

    Plugin Verdict Risk Under the default
    nvim-cmp safe 0 allowed
    nvim-lspconfig safe 18 allowed
    mason.nvim review 23 allowed
    telescope.nvim review 25 allowed
    conform.nvim review 26 allowed
    plenary.nvim review 33 allowed
    which-key.nvim review 33 allowed
    lualine.nvim review 41 allowed
    nvim-treesitter review 41 allowed
    lazy.nvim review 53 allowed
    gitsigns.nvim review 58 allowed
    LuaSnip prompt 70 approve once

    Nothing is wrong with gitsigns or LuaSnip. They really do eval, read the environment and ship native artifacts, and the scanner is right to say so. The point of approving is that you look once, :Aegis approve, and then find out if that ever changes.

    If that trade is not the one you want, fail_on = "block" reduces this to build-hook malware only.

    load_unknown starts permissive

    On a cold cache, load_unknown = "block" would refuse your entire config on first run. The default allows uncached plugins once, scans them in the background, and enforces from the next startup. Once :Aegis scan has warmed the cache, set it to "block".

    strict_clone

    Off by default. When on, a new plugin is cloned into a throwaway directory and scanned there, so a rejected tree never occupies plugin.dir at all. Costs one extra shallow fetch per new plugin.

    With it off, the tree does land in plugin.dir before the scan. But nothing has executed at that point (checkout, helptags and build all come later, and runtimepath is untouched), and a rejected clone is deleted.

    Commands

    Command Effect
    :Aegis / :Aegis status verdict, risk and capabilities per plugin
    :Aegis scan scan every plugin (:Aegis! scan ignores the cache)
    :Aegis scan <plugin> scan one
    :Aegis report <plugin> risk flags and file:line evidence
    :Aegis approve <plugin> allow the installed commit, and only that commit
    :Aegis approve! <plugin> allow the commit a pending update wants to move to
    :Aegis revoke <plugin> drop an approval
    :Aegis clear clear the verdict cache, keep approvals
    :checkhealth aegis confirm the gates and hardening are actually wired

    Approvals are pinned to a commit sha. The next update moves the sha, the approval stops applying, and the new tree is gated again.

    Bare :Aegis approve only ever blesses the commit that is already installed, so answering a blocked update reflexively can never wave the incoming commit through. When you have looked at an update and do want it, :Aegis approve! (after the :Lazy update that blocked, in the same session) approves the target the update is moving to; the next :Lazy update completes it.

    What this does not protect you from

    • A plugin aegis scores as benign. This is static capability analysis, not proof of safety. Novel obfuscation, logic bombs, and malice expressed in ordinary-looking API calls all pass.
    • A compromised scanner. aegis parses hostile input on your machine. Run it sandboxed if that matters to you: bwrap --unshare-all --ro-bind ....
    • Your own config. init.lua, and the config/init/opts functions in your specs, are yours and are never scanned.
    • Anything installed before adoption, until the cache is warm. See load_unknown.
    • Non-lazy plugin managers. For vim.pack, use pakku.nvim, which has the same aegis integration.
    • A gate that is disabled. :checkhealth aegis tells you which are live.

    Tests

    tests/run.sh          # all
    tests/run.sh 03       # one

    Real nvim, real lazy.nvim, real aegis, real git repos over file://. Nothing is mocked. Each test checks that the malicious fixture is actually stopped:

    Suite Asserts
    01_clone_gate malicious plugin is removed from the plugin root
    02_build_gate its build hook never runs, a clean plugin's does
    03_update_gate a plugin that goes bad on update never moves its worktree
    04_load_gate an already-installed bad tree never reaches runtimepath
    05_hardening lazy_opts() closes pkg/rocks/submodules, remote policy holds
    06_strict_clone a rejected tree never occupies plugin.dir
    07_approval_recovery a refused plugin can still be inspected and approved

    lazy.nvim is fetched into a cache dir on first run; point AEGIS_TEST_LAZY at an existing checkout to skip that.

    CI runs stylua, luacheck, and the e2e suite on Neovim stable and nightly. The e2e job downloads a pinned aegis-cli release (AEGIS_VERSION in .github/workflows/ci.yml) and checks it against the release's checksums.txt, so a scoring change in aegis only reaches this repo through a deliberate version bump.

    License

    MIT

    About

    Supply-chain gate for lazy.nvim — gates clone, update, build and load on an aegis-cli verdict for that exact commit

    Resources

    Stars

    2 stars

    Watchers

    0 watching

    Forks

    Releases

    Packages

    Contributors

    Languages

    0