Skip to main content

Language Server Protocol server for AppArmor profiles

Project description

AppArmor Language Server (apparmor-language-server)

apparmor-language-server PyPI - Version CI Coverage License: GPL 3.0 Python 3.10+

A full-featured Language Server Protocol server for editing AppArmor profiles, written in Python using pygls.


Features

Feature Details
Completion Rule keywords with snippets, all Linux capabilities, network families/types, signal names, ptrace/mount/dbus/unix permissions, file permission strings (r, rw, rix, rPx, …), @{variable} names, #include abstraction paths, live filesystem path completion
Hover Rich Markdown docs for every keyword, capability, permission char, network family, profile flag and variable
Goto Definition Jump from #include <…> or include <…> to the target file; jump to profile definitions by name; follow exec transitions (px, cx, Px, …) and change_profile rules to the target profile by name or attachment path
Call Hierarchy Incoming calls: which profiles exec-transition or change_profile into a given profile; outgoing calls: which profiles a given profile transitions to. Triggered from a profile name or from an exec-transition target token
Document Symbols Full outline: all profiles, hats, capabilities, file rules, includes and variables
Workspace Symbols Search profiles across all open documents
Diagnostics / Linting Unknown capabilities, network families/types, signal permissions/names, ptrace permissions, dbus permissions, unix socket types/permissions, mqueue types/permissions, io_uring permissions, userns permissions, mount options, rlimit resources/values; dangerous unconfined exec (ux/Ux/pux/PUx/cux/CUx); conflicting profile mode flags; invalid flags=(error=…) errno values; empty profiles; duplicate and conflicting capabilities; duplicate permissions within a single signal/ptrace/dbus/unix/mqueue/io_uring/userns rule; duplicate signal names within a set=(…) list; conflicting allow+deny; undefined variables and bool variables; unused variables; unused preamble includes; missing include/abi targets; missing abi declaration; missing tunables/global include; abstractions/nameservice used where nameservice-strict suffices; missing owner qualifier on @{HOME}, /home/, @{run}/user/, /run/user/, and /tmp/ file rules; unclosed profiles; mutually exclusive file permissions (w+a); multiple exec transition modes; exec target without exec transition; exec transition with deny; bare x without deny; relative alias paths; pivot_root paths without trailing /; network netlink restricted to dgram/raw; profile name does not match document filename; missing include if exists <local/…> at end of profile; also surfaces errors from apparmor_parser itself when available (skipped automatically for abstraction and tunables files)
Formatting Normalise indentation, remove trailing whitespace, sort capabilities alphabetically, sort parenthesised lists, ensure trailing commas on all rules, normalise #includeinclude, collapse multiple blank lines, align consecutive file rules so the path/permission column lines up, wrap long rules with a continuation indent, format dbus rules clause-by-clause, preserve inline comments, preserve or normalise file rule style (implicit / explicit / node-local)
Range Formatting Format a selected region only
References Find all references to the identifier or variable under the cursor across all open documents (excludes path components and comments)
Document Links include and abi paths rendered as inline hyperlinks; clicking navigates to the target file
Document Highlight Highlight all occurrences of the word under the cursor
Semantic Tokens Full syntax highlighting pushed to the editor: rule keywords, qualifiers (allow/deny/audit/priority=N), identifiers (profile names, capability names, network families, …), paths, permission strings, key=value parameters, operators (->, +=, …), variable references (@{VAR}, ${BOOL}), comments, and numbers — enables theme-aware colouring without relying on TextMate grammar files
Rename Rename a variable (@{VAR}) in place across all open documents; prepareRename validates the target before the operation and prevents renaming of non-variable tokens
Folding Range Collapse profile, hat, if, and qualifier blocks in the editor gutter
Selection Range Expand selection from rule → enclosing block → profile on each keypress
Code Actions Quick fixes for flagged diagnostics — see the Code actions reference below

Contributing and Issues

We welcome contributions! Please feel free to open a Merge Request.

If you find a bug or have a feature request, please check the Issue tracker to see if it has already been reported. If not, feel free to open a new issue.


Installation

From snap

sudo snap install apparmor-language-server

The snap provides the apparmor-language-server, apparmor-lint, and apparmor-format commands. Access to /etc/apparmor.d for resolving include directives and indexing system profiles is granted automatically.

Get it from the Snap Store

From PyPI

pip install apparmor-language-server

From source

git clone https://gitlab.com/apparmor/apparmor-language-server
cd apparmor-language-server
pip install .

Or install dependencies directly without building a package:

pip install pygls lsprotocol
python -m apparmor_language_server          # stdio mode
python -m apparmor_language_server --port 2087  # TCP mode on 127.0.0.1:2087

Running the server

stdio (default — used by most editors)

apparmor-language-server
# or
python -m apparmor_language_server

TCP (useful for debugging)

apparmor-language-server --port 2087

Standalone linter (apparmor-lint)

The package also ships a apparmor-lint command-line tool that runs the same parser and diagnostic checks as the language server — useful in CI, pre-commit hooks, or when you just want a quick check from the shell without firing up an editor.

# Lint one or more files
apparmor-lint /etc/apparmor.d/usr.bin.foo
apparmor-lint profile another-profile

# Read from stdin
cat profile | apparmor-lint -

# Skip the external apparmor_parser cross-check
apparmor-lint --no-parser profile

# Machine-readable output for CI
apparmor-lint --format json profile

Output format

The default pretty format is GCC-compatible so editors and tools that already parse cc(1) output (Vim quickfix, Emacs compile, grep -nH, …) work out of the box:

profile:3:3: error: Unknown capability 'bad_cap_xyz'. … [unknown-capability] (apparmor-language-server)
profile:5:3: error: Network rule: netlink may only specify type 'dgram' or 'raw' (got 'stream'). [netlink-type-restricted] (apparmor-language-server)

--format json emits an array of records, each with path, uri, severity, message, code, source, line, column, end_line, and end_column — handy for piping into jq or aggregating across files.

Options

Flag Meaning
paths… One or more files to lint, or - to read from stdin
--no-parser Skip the external apparmor_parser -Q -K cross-check
--apparmor-parser PATH Use a specific apparmor_parser binary (default: $PATH lookup)
-I DIR, --include-path DIR Extra directory to search for include/abi targets (repeatable)
--format {pretty,json} Output format (default: pretty)
-q, --quiet Only show error-severity diagnostics

Exit codes

Code Meaning
0 Clean — no error-severity diagnostics (warnings, info, hints permitted)
1 At least one error-severity diagnostic was emitted
2 The CLI itself could not run (file missing, argument is a directory, etc.)

Library API

apparmor_language_server.lint also exposes the linter as a Python API for embedding in other tools:

from apparmor_language_server.lint import lint_file, lint_text

# Returns dict[str, list[lsprotocol.types.Diagnostic]] keyed by URI.
diags = lint_file(Path("profile"), run_apparmor_parser=False)
diags = lint_text("profile x { /foo r, }\n")

Standalone formatter (apparmor-format)

The package also ships an apparmor-format command-line formatter that uses the same formatter and renderer behavior as the language server.

# Print formatted output to stdout
apparmor-format profile

# Rewrite a file in place
apparmor-format --in-place profile

# Check whether files would change
apparmor-format --check profile another-profile

# Show a unified diff of what would change (useful in CI and pre-commit hooks)
apparmor-format --diff profile another-profile

# Read from stdin
cat profile | apparmor-format -

Modes

Flag Meaning
(default) Write formatted output to stdout
-i, --in-place Rewrite files in place
--check Exit non-zero if any input would be reformatted, printing a would reformat: line per file
--diff Print a unified diff of formatting changes to stdout; exit non-zero if any input would change

Formatter options

These flags cover the formatter-specific behaviour; include resolution uses the parser defaults and missing includes do not prevent formatting.

Flag Meaning
--indent-width N Spaces per indent level (default: 2)
--max-line-length N Wrap rules longer than N columns; 0 disables wrapping
--file-rule-style {node-local,implicit,explicit} Preserve parsed file rule syntax, or force implicit / explicit rendering

Exit codes

Code Meaning
0 Formatting succeeded, or --check/--diff found no changes
1 --check or --diff found one or more inputs that would change
2 The CLI itself could not run (bad arguments, missing files, etc.)

Server configuration

Environment variables

Variable Default Effect
APPARMOR_LSP_LOG_LEVEL INFO Log verbosity: DEBUG, INFO, WARNING, ERROR

LSP workspace settings

These are passed to the server via the standard LSP workspace/didChangeConfiguration notification, nested under the apparmor key. How you set them depends on your editor (see the examples below each editor section).

Setting Type Default Effect
apparmor.diagnostics.enable boolean true Enable or disable all diagnostic (linting) checks
apparmor.baseDir string "/etc/apparmor.d" Base directory for AppArmor profiles. Passed as --base to apparmor_parser. Also used as the default value for apparmor.includeSearchPaths when that setting is not configured. Defaults to /var/lib/snapd/hostfs/etc/apparmor.d when running as a snap.
apparmor.parserConfigFile string "" Path to the apparmor_parser configuration file, passed as --config-file. Leave empty to auto-detect: under snap confinement the host /etc/apparmor/parser.conf is used automatically; outside snap no --config-file is passed.
apparmor.includeSearchPaths string[] [] Extra directories to search when resolving include and abi paths, prepended ahead of apparmor.baseDir. When empty, apparmor.baseDir is used as the sole search directory.
apparmor.profilesSubdirs string[] ["apparmor.d", "profiles/apparmor.d"] Subdirectories of the workspace root to index for workspace symbols; each entry is resolved relative to the workspace root. Set to [""] or ["."] to index the whole workspace. Multiple entries are all indexed.
apparmor.apparmorParserPath string "" Path to the apparmor_parser binary. Leave empty to auto-detect from $PATH. Set to a specific path (e.g. /usr/sbin/apparmor_parser) to pin a particular version. When the binary is found, the server runs apparmor_parser -Q -K against each saved profile file and surfaces any errors as diagnostics. Files with no top-level profiles (abstractions, tunables, ABI files) are skipped automatically.
apparmor.formatting.maxLineLength integer 100 Wrap rules longer than this many columns with a continuation indent; 0 disables wrapping.
apparmor.formatting.fileRuleStyle string "node-local" How to render file rules: "node-local" preserves the parsed style, "implicit" forces path perms, form, "explicit" forces file perms path, form.
apparmor.formatting.sortLists boolean true Sort comma- or space-separated lists into alphabetical order: capability names, profile flags, and parenthesised permission/signal sets (e.g. (receive send)).
apparmor.formatting.normalizeInclude boolean true Rewrite #include directives as include (the preferred no-hash form).
apparmor.formatting.maxBlankLines integer 1 Collapse runs of consecutive blank lines to at most this many. 0 strips all blank lines. -1 disables collapsing (blank lines are preserved as-is).

Editor configuration

Neovim (with nvim-lspconfig)

-- In your init.lua or a plugin file
local lspconfig = require('lspconfig')
local configs   = require('lspconfig.configs')

-- Register the server if it is not already known
if not configs.apparmor_language_server then
  configs.apparmor_language_server = {
    default_config = {
      cmd         = { 'apparmor-language-server' },  -- or { 'python', '-m', 'apparmor_language_server' }
      filetypes   = { 'apparmor' },
      root_dir    = lspconfig.util.root_pattern('.git', '/etc/apparmor.d'),
      single_file_support = true,
      settings    = {},
    },
  }
end

lspconfig.apparmor_language_server.setup({
  on_attach = function(client, bufnr)
    -- Enable format-on-save
    vim.api.nvim_create_autocmd('BufWritePre', {
      buffer = bufnr,
      callback = function()
        vim.lsp.buf.format({ async = false })
      end,
    })
  end,
})

-- Tell Neovim about the AppArmor filetype
vim.filetype.add({
  pattern = {
    ['/etc/apparmor.d/.*']     = 'apparmor',
    ['/etc/apparmor/.*%.conf'] = 'apparmor',
    ['.*%.apparmor']           = 'apparmor',
  },
})

VS Code

A VS Code extension is included in editors/vscode/. To build and install it locally:

cd editors/vscode
npm install
npm run compile
npx vsce package --no-dependencies   # produces apparmor-language-server-*.vsix
code --install-extension apparmor-language-server-*.vsix

The extension locates the server automatically in this order:

  1. apparmor.serverPath setting — explicit path to the apparmor-language-server executable.
  2. apparmor-language-server on PATH — covers system installs, pip install --user, and activated virtualenvs.
  3. The active Python interpreter from the Python extension, if installed.
  4. apparmor.pythonPath setting, or python3 as a last resort.

Files matching **/apparmor.d/** are recognised automatically. Use the AppArmor: Restart Language Server command palette entry to restart the server after installing or upgrading the Python package.

Emacs (with eglot)

(add-to-list 'auto-mode-alist '("/etc/apparmor\\.d/.*" . apparmor-mode))
(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs
               '(apparmor-mode . ("apparmor-language-server"))))

Emacs (with lsp-mode)

(with-eval-after-load 'lsp-mode
  (lsp-register-client
   (make-lsp-client
    :new-connection (lsp-stdio-connection '("apparmor-language-server"))
    :major-modes '(apparmor-mode)
    :server-id 'apparmor-language-server)))

Helix

In ~/.config/helix/languages.toml:

[[language]]
name              = "apparmor"
scope             = "source.apparmor"
file-types        = ["apparmor", { glob = "/etc/apparmor.d/**" }]
language-servers  = ["apparmor-language-server"]
comment-token     = "#"
indent            = { tab-width = 2, unit = "  " }

[language-server.apparmor-language-server]
command = "apparmor-language-server"

Sublime Text (with LSP package)

In LSP.sublime-settings:

{
  "clients": {
    "apparmor-language-server": {
      "enabled": true,
      "command": ["apparmor-language-server"],
      "selector": "source.apparmor"
    }
  }
}

Formatting options

The formatter respects the editor's tabSize setting (passed via the LSP DocumentFormattingParams). All other options are controlled via the apparmor.formatting.* server configuration keys described in the server configuration table above; the apparmor-format CLI exposes the same options as command-line flags.


Diagnostics reference

Code Severity Meaning
parse-error Error Syntax error detected by parser
unknown-capability Error Capability not in man 7 capabilities list
unknown-flag Error Unrecognised profile flag in flags=(…)
unknown-network-qualifier Warning Unknown network family or socket type
unknown-keyword Warning Unrecognised rule keyword
dangerous-exec Warning ux/Ux/pux/PUx/cux/CUx allows unconfined exec
empty-profile Warning Profile body has no rules
duplicate-capability Warning Capability listed more than once within the same rule, or already declared in an earlier rule in the same profile
duplicate-permission Warning Permission token listed more than once in a single signal/ptrace/dbus/unix/mqueue/io_uring/userns rule
conflicting-capability Warning Same capability both allowed and denied
undefined-variable Warning @{VAR} used but never defined
undefined-bool-variable Warning ${BOOL_VAR} referenced in an if condition but never defined
unused-variable Warning @{VAR} or ${BOOL_VAR} defined in the global scope but never referenced anywhere in the document (only checked in profile files, not abstractions)
unused-include Warning Preamble include whose contributed variables are all unreferenced in the document, or that contributes no preamble-valid content at all (e.g. a rule abstraction mistakenly placed outside a profile body); only checked in profile files, not abstractions
missing-tunables-global Warning Profile file does not include <tunables/global> in its preamble
missing-abi-declaration Warning Profile file does not have an abi declaration in its preamble
missing-include Warning Include target not found on disk
missing-abi Warning ABI target not found on disk
prefer-abstractions-nameservice-strict Warning include <abstractions/nameservice> used where abstractions/nameservice-strict suffices; the strict variant covers only local lookups (passwd, nsswitch.conf, systemd user database) and should be preferred unless the program genuinely requires network-based name resolution such as DNS or LDAP
unknown-signal-permission Warning Invalid permission in signal rule
unknown-signal-name Warning Unknown signal name in signal set=(…)
duplicate-signal-name Warning Signal name listed more than once in a set=(…) list
unknown-ptrace-permission Warning Invalid permission in ptrace rule
perm-conflict-write-append Error w and a are mutually exclusive in a file rule
multiple-exec-modes Error More than one exec transition mode (e.g. ix, px, cx) in a single file rule
exec-target-without-transition Error -> target present but neither an exec transition mode (e.g. px, cx, ix) nor l (link) permission gives it a meaning
deny-with-exec-transition Error Exec transition mode (e.g. ix, px) used with the deny qualifier — use deny x instead
bare-x-without-deny Error Bare x permission used without the deny qualifier — use an exec transition mode (ix, px, cx, …)
missing-owner-qualifier Warning File rule accessing @{HOME}, /home/, @{run}/user/, /run/user/, or /tmp/ paths without the owner qualifier; the owner qualifier restricts access to files owned by the current process owner; handles alternations and path normalisation
bare-proc-wildcard Warning File rule using a bare * or [0-9]* in the PID position of a /proc/ path (use @{pids} for any process or @{pid} for the current process), or in the TID position after .../task/ (use @{tid})
suggest-variable Hint File rule path contains a literal segment that matches a plain (non-glob) value of a variable defined in scope; suggests replacing the literal with the variable reference
broad-home-access Warning File rule grants recursive home access (@{HOME}/** or @{HOMEDIRS}/**) without dot-file restriction, exposing SSH keys, GPG keys, and browser credentials; offers include <abstractions/private-files-strict> or @{HOME}/[^.]** as alternative fixes
home-dot-file-exclusion Warning File rule @{HOME}/** (or /*) paired with a deny @{HOME}/.** rule; the pair can be collapsed to the single idiomatic form @{HOME}/[^.]**
prefer-attach-disconnected-path Warning Profile uses flags=(attach_disconnected) without a .path= qualifier; the qualified form attach_disconnected.path=/att/<profile-name>/ scopes the disconnected-path namespace and makes corresponding rules auditable
invalid-attach-disconnected-path Error flags=(attach_disconnected.path) or flags=(attach_disconnected.ipc) used without a =VALUE; apparmor_parser rejects bare forms of these flags
wrong-attach-disconnected-path Warning Profile uses flags=(attach_disconnected.path=…) where the path does not start with /, or does not follow the /att/<profile-name>/ convention
inline-comment-missing-space Warning Inline comment (trailing comment on a rule or directive) does not have a space after # (e.g. capability net_admin, #reason instead of # reason)
file-rule-subsumed Warning File rule whose path and permissions are entirely covered by another rule in the same profile body, or by a rule from a transitively included abstraction
rule-subsumed Warning Capability, network, signal, ptrace, userns, io_uring, mqueue, unix, dbus, mount, umount, remount, pivot_root, or change_profile rule entirely covered by a broader sibling rule or a rule from a transitively included abstraction; also flags any non-deny rule covered by an all, rule
allow-deny-conflict Error allow and deny qualifiers used together on the same rule
conflicting-profile-modes Error More than one profile mode flag (enforce/complain/kill/default_allow/unconfined/prompt) set together
invalid-error-flag-value Warning flags=(error=…) value is not a valid E… errno name
alias-relative-path Warning alias source/target is not an absolute path
unknown-mount-option Warning Mount option not in the documented mount(8) flag list
unknown-rlimit-resource Error Resource name not recognised by setrlimit(2)
invalid-rlimit-value Warning rlimit value/unit doesn't match the resource family (size, time, integer, nice range -20..19)
unknown-dbus-permission Warning Invalid permission in dbus rule
dbus-bind-in-message-rule Error bind permission used in a dbus message rule (path/interface/member/peer)
dbus-send-recv-in-service-rule Error send/receive used in a dbus service rule (name=)
dbus-eavesdrop-with-conds Error eavesdrop used with conditionals other than bus=
unknown-unix-permission Warning Invalid permission in unix socket rule
unknown-unix-type Warning type= not in stream/dgram/seqpacket
unknown-mqueue-permission Warning Invalid permission in mqueue rule
unknown-mqueue-type Warning type= not in posix/sysv
mqueue-posix-name-shape Error POSIX mqueue name must start with /
mqueue-sysv-name-shape Error SysV mqueue name must be a positive integer
unknown-io-uring-permission Warning io_uring permission not in sqpoll/override_creds/cmd
unknown-userns-permission Warning userns permission other than create
netlink-type-restricted Error network netlink may only specify type dgram or raw
pivot-root-trailing-slash Warning pivot_root path doesn't end with / (paths refer to directories)
profile-name-mismatch Warning Top-level profile name does not match the document filename (e.g. profile /usr/bin/foo should be in file usr.bin.foo)
duplicate-profile-name Error Profile name is defined in more than one document; the later-loaded definition will silently replace the earlier one
missing-local-include Warning Top-level profile does not end with include if exists <local/…>, preventing local customisation
apparmor-parser-error Error Error reported by apparmor_parser -Q -K; attached to the file and line cited by the parser (may be an included abstraction)

file-rule-subsumed and rule-subsumed — examples

profile example /usr/bin/example {
    /usr/bin/foo r,       # flagged: subsumed by the rule below
    /usr/bin/*   r,

    capability net_bind_service,          # flagged: subsumed by the rule below
    capability net_bind_service net_admin,
}

Known limitations

Rules that appear inside qualifier blocks (owner { }, audit { }, deny { }, …) or inside if defined conditional blocks are excluded from the comparison. Rules contributed by transitively included abstractions are checked — the diagnostic message names the top-level include directive that introduced the broader rule. Rules inside ProfileNode bodies found in included files (sub-profiles) are not compared, as those represent separate confinement domains. This applies to both file-rule-subsumed and rule-subsumed.

For file-rule-subsumed: paths containing variable references (@{VAR}) are compared only when both rules have identical variable tokens at the same positions — @{HOME}/foo is not detected as subsumed by /home/user/* even if @{HOME} always expands to /home/user. When both rules have partial globs in the same path-component position (e.g. foo* vs foo?), subsumption cannot be determined structurally and is conservatively skipped.

Suppressing diagnostics

Place a # apparmor-lint: ignore=<code> comment at the end of a line where a rule is defined, or on the line immediately before a rule, to silence one or more diagnostics for that rule only. Multiple codes can be listed comma-separated. A blank line between the comment and the rule cancels the annotation. The space after # is optional.

profile example /usr/bin/example {
  # This rule intentionally uses unconfined exec — required for the wrapper.
  /usr/bin/helper ux, # apparmor-lint: ignore=dangerous-exec

  # Suppress multiple codes on one rule (ux on a path containing an
  # undefined variable).
  # apparmor-lint: ignore=dangerous-exec,undefined-variable
  @{helper_path} ux,

  # The annotation below is cleared by the blank line and has no effect.
  # apparmor-lint: ignore=unknown-capability

  capability not_a_real_cap,
}

Annotations at the top of a file

The preamble is the uninterrupted block of comment lines at the very start of a file — it ends at the first blank line or the first non-comment statement, whichever comes first. Preamble comments are never attached to any node, which protects copyright notices, SPDX identifiers, and other licence headers from being accidentally deleted by code actions.

Document-wide suppression: A # apparmor-lint: ignore= annotation placed in the preamble silences that diagnostic code for the entire file. This is the recommended way to suppress diagnostics such as missing-abi-declaration and missing-tunables-global that are not attached to a specific statement:

# Copyright (C) 2024 Example Corp
# SPDX-License-Identifier: GPL-2.0
# apparmor-lint: ignore=missing-abi-declaration,missing-tunables-global
include <my-custom-tunables>
profile p { ... }

Multiple codes may be listed on one line (comma-separated) or spread across several # apparmor-lint: ignore= lines in the preamble block — all are applied. Note that parse-error and apparmor-parser-error cannot be suppressed this way.

Important: the preamble ends as soon as a non-comment statement appears. If the first line of the file is a non-comment (e.g. abi <abi/4.0>,), any annotation on the next line is already outside the preamble and will be treated as a node-level annotation on the following statement — not as a document-wide suppression. To use document-wide suppression alongside an abi declaration, place the annotation before the abi line:

# apparmor-lint: ignore=missing-tunables-global
abi <abi/4.0>,
include <my-custom-tunables>
profile p { ... }

Node-level annotation after a copyright block: To apply a # apparmor-lint: ignore= annotation to only the first statement in the file (rather than the whole document), place a blank line between the preamble and the annotation so it falls outside the preamble zone:

# Copyright (C) 2024 Example Corp
# SPDX-License-Identifier: GPL-2.0

# apparmor-lint: ignore=missing-tunables-global
include <my-custom-tunables>

Code actions reference

Quick fixes are offered automatically when a diagnostic appears on a line. Editors typically show them via a lightbulb or a <leader>ca / Ctrl+. keybinding.

Diagnostic code Action title Effect
perm-conflict-write-append Remove 'a' (keep 'w') Removes the a (append) permission from the file rule
perm-conflict-write-append Remove 'w' (keep 'a') Removes the w (write) permission from the file rule
file-rule-subsumed Remove subsumed rule ⭐ Deletes the redundant file rule (and any attached preceding comments)
rule-subsumed Remove subsumed rule ⭐ Deletes the redundant rule (and any attached preceding comments)
duplicate-capability Remove duplicate capabilities ⭐ Removes the duplicate capability tokens from the rule; deletes the entire rule if all of its capabilities are duplicates
duplicate-permission Remove duplicate permissions ⭐ Removes all duplicate permission tokens from the rule, preserving the first occurrence
duplicate-signal-name Remove duplicate signal names ⭐ Removes all duplicate signal names from the set=(…) list, preserving the first occurrence
unused-variable Remove unused variable '…' ⭐ Deletes the variable definition (and any attached preceding comments)
unused-include Remove unused include '…' ⭐ Deletes the include directive
prefer-abstractions-nameservice-strict Replace with 'include <abstractions/nameservice-strict>' ⭐ Replaces the abstractions/nameservice include path with abstractions/nameservice-strict
missing-owner-qualifier Add 'owner' qualifier ⭐ Inserts owner before the path token in the file rule
bare-proc-wildcard Replace * with @{pids} ⭐ / @{pid} / @{tid} Replaces the bare wildcard with the appropriate PID or TID variable directly within the path token
suggest-variable Replace literal with variable ⭐ Replaces the literal path segment with the matching variable reference; one action per variable when multiple match
broad-home-access Add 'include <abstractions/private-files-strict>' ⭐ / Replace with '@{HOME}/[^.]**' Two alternative fixes: insert abstractions/private-files-strict (denies specific sensitive paths), or rewrite the path to @{HOME}/[^.]** (excludes all dot files, allowing selective re-grants)
home-dot-file-exclusion Replace with '@{HOME}/[^.]**' and remove deny rule ⭐ Rewrites the allow path to use the [^.] character class and deletes the now-redundant deny @{HOME}/.** rule
invalid-attach-disconnected-path Replace with 'attach_disconnected.path=…' ⭐ Replaces the bare attach_disconnected.path flag with the qualified attach_disconnected.path=/att/<profile-name>/ form
prefer-attach-disconnected-path Replace with 'attach_disconnected.path=…' ⭐ Replaces the bare attach_disconnected flag with the qualified attach_disconnected.path=/att/<profile-name>/ form
wrong-attach-disconnected-path Replace path with '/att/…/' ⭐ Replaces the non-conventional path value with /att/<profile-name>/
inline-comment-missing-space Add space after '#' ⭐ Inserts a space immediately after the # character(s) in the comment
missing-abi-declaration Add 'abi <abi/N.M>,' ⭐ Inserts the highest-numbered ABI declaration at the top of the preamble
missing-tunables-global Add 'include <tunables/global>' ⭐ Inserts the tunables/global include after any ABI declaration
profile-name-mismatch Rename profile to '…' ⭐ Replaces the profile name token with the name derived from the document filename
missing-local-include Add 'include if exists <local/…>' ⭐ Inserts include if exists <local/NAME> before the profile's closing brace
unknown-capability, unknown-signal-name, unknown-signal-permission, unknown-ptrace-permission, unknown-dbus-permission, unknown-unix-permission, unknown-unix-type, unknown-mqueue-permission, unknown-mqueue-type, unknown-io-uring-permission, unknown-userns-permission, unknown-mount-option, unknown-rlimit-resource, unknown-network-qualifier, unknown-flag, unknown-keyword Replace 'X' with 'Y' ⭐ Replaces the unknown token with the closest valid alternative (up to three suggestions offered, ranked by similarity)
undefined-variable Replace '@{X}' with '@{Y}' ⭐ Replaces the undefined variable reference with the closest defined variable name (up to three suggestions offered, ranked by similarity)
any rule diagnostic Suppress 'code' for this rule Inserts a # apparmor-lint: ignore=code comment immediately before the rule to silence that diagnostic

⭐ Marked as the preferred action (used by editor auto-fix commands).


Architecture

apparmor_language_server/
├── __init__.py         – package metadata
├── __main__.py         – python -m apparmor_language_server entry point
├── server.py           – pygls LSP server, all handler registration
├── indexer.py          – workspace indexer
├── parser.py           – line-oriented AST parser (profiles, rules, …)
├── nodes.py            – AST node dataclasses and visitor utilities (iter_children, walk, NodeVisitor)
├── constants.py        – capabilities, keywords, permissions, abstractions, …
├── render.py           – renders individual AST nodes back to text (RenderOptions, render_node dispatch)
├── completions.py      – context-aware completion provider
├── diagnostics.py      – linting / diagnostic checks
├── code_actions.py     – quick-fix code action provider
├── formatting.py       – AST-driven auto-formatter (returns TextEdits; calls render.py per node)
├── hover.py            – hover documentation provider
├── semantic_tokens.py  – semantic token provider (syntax highlighting)
├── lint.py             – standalone `apparmor-lint` CLI (parser + diagnostics, GCC/JSON output)
├── format.py           – standalone `apparmor-format` CLI (formatter, stdout/in-place/check/diff)
└── docs.py             – helpers for consistent hover/completion docs

Adding a new node type

When a new node type is added to nodes.py, the dispatch tables in hover.py, render.py, and semantic_tokens.py all need a corresponding entry, plus an appropriate implementation function in each file (see sections below).

Adding new checks

Create a new _check_* function in diagnostics.py and call it from _check_node(). Each check receives the AST node and appends Diagnostic objects to the list.

Adding new hover documentation

Add a _hover_<NodeType>(node, line_text, ch) -> Optional[Hover] function in hover.py and register it in the _HOVER_DISPATCH table at the bottom of that file. Variable references (@{VAR}) within any node are resolved before the dispatch, so handlers only need to cover node-specific keywords and fields.

Adding new renderer support

Add a render_<NodeType> function in render.py following the (node, opts: RenderOptions) -> str signature, then register it in the _RENDER_DISPATCH table at the bottom of that file. RenderOptions carries sort (sort parenthesised lists) and file_rule_style.

Adding new semantic token support

Add a _tokens_for_<NodeType> function in semantic_tokens.py following the (node: NodeType) -> list[_Token] signature, then register it in the _DISPATCH table at the bottom of that file. Emit individual tokens with _add(tokens, raw, start_line, offset, length, token_type[, modifiers]). Use RE_ANY_VAR from constants.py to highlight variable and boolean-variable references within values.

Adding new code actions

Add a handler in code_actions.py: check diag.code in get_code_actions(), find the relevant AST node with walk(), and return a CodeAction with a WorkspaceEdit containing the corrective TextEdits.

Adding new completions

Add entries to the relevant _complete_* function in completions.py, or extend the get_completions() dispatcher with a new regex trigger.


Development

# Install dev dependencies (includes pytest, ruff, ty, etc.)
pip install -e ".[dev]"

# Run tests
pytest tests/ -v

# Run tests with coverage, outputting both terminal and JSON reports
pytest --cov=apparmor_language_server --cov-report=term --cov-report=json

# Lint and format
ruff check apparmor_language_server/
ruff format apparmor_language_server/

# Type-check
ty check apparmor_language_server/

# Run the server in TCP mode for interactive debugging
python -m apparmor_language_server --port 2087

Licence

GPL 3.0 or later

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

apparmor_language_server-0.8.2.tar.gz (307.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

apparmor_language_server-0.8.2-py3-none-any.whl (182.1 kB view details)

Uploaded Python 3

File details

Details for the file apparmor_language_server-0.8.2.tar.gz.

File metadata

  • Download URL: apparmor_language_server-0.8.2.tar.gz
  • Upload date:
  • Size: 307.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for apparmor_language_server-0.8.2.tar.gz
Algorithm Hash digest
SHA256 3e8cf44517b1a01c050bb3c0d916db851bd1a6bc3ce3c79a80881b79f380e30b
MD5 d264300de16c0fa2829024e9f1fe9f2d
BLAKE2b-256 99b1a13619022aa6d7d6510484d78b045cac694eea34212898f229e5a671e55f

See more details on using hashes here.

File details

Details for the file apparmor_language_server-0.8.2-py3-none-any.whl.

File metadata

File hashes

Hashes for apparmor_language_server-0.8.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a869642e02228817b4399bdd75d1ca0a4ac28df2eab126e92c167b1647b280ea
MD5 771c5e569ba7472203b66696ef0a80fd
BLAKE2b-256 2d1bdf76fbfda41a799e93a7534351dae4bd47e52c33d672863ca2705b79a7ea

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page