Language Server Protocol server for AppArmor profiles
Project description
AppArmor Language Server (apparmor-language-server)
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 #include → include, 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.
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:
apparmor.serverPathsetting — explicit path to theapparmor-language-serverexecutable.apparmor-language-serveronPATH— covers system installs,pip install --user, and activated virtualenvs.- The active Python interpreter from the Python extension, if installed.
apparmor.pythonPathsetting, orpython3as 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-subsumedandrule-subsumed— examplesprofile 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 insideif definedconditional 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 insideProfileNodebodies found in included files (sub-profiles) are not compared, as those represent separate confinement domains. This applies to bothfile-rule-subsumedandrule-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}/foois 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*vsfoo?), 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e8cf44517b1a01c050bb3c0d916db851bd1a6bc3ce3c79a80881b79f380e30b
|
|
| MD5 |
d264300de16c0fa2829024e9f1fe9f2d
|
|
| BLAKE2b-256 |
99b1a13619022aa6d7d6510484d78b045cac694eea34212898f229e5a671e55f
|
File details
Details for the file apparmor_language_server-0.8.2-py3-none-any.whl.
File metadata
- Download URL: apparmor_language_server-0.8.2-py3-none-any.whl
- Upload date:
- Size: 182.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a869642e02228817b4399bdd75d1ca0a4ac28df2eab126e92c167b1647b280ea
|
|
| MD5 |
771c5e569ba7472203b66696ef0a80fd
|
|
| BLAKE2b-256 |
2d1bdf76fbfda41a799e93a7534351dae4bd47e52c33d672863ca2705b79a7ea
|