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; available AppArmor abstraction that covers ≥85% of the profile's inline rules (abstraction-available, with a code action to insert the include and remove redundant rules); 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 variable (@{VAR}, ${VAR}) or profile under the cursor across all open and indexed documents: a variable's definition and uses; a profile's declaration, exec-transition (px -> name), change_profile/pivot_root targets and signal/ptrace/peer=(label=…) peers. Keywords, permissions, paths and comments are not matched |
| Document Links | include and abi paths rendered as inline hyperlinks; clicking navigates to the target file |
| Document Highlight | On a variable or profile, highlight its references in the file (the same set Find References returns), marking the definition as a write and uses as reads; elsewhere, 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; dangerous unconfined-exec permissions and confinement-escaping capabilities are flagged as warnings, and xattrs=(…) pairs are highlighted |
| Rename | Rename a variable (@{VAR}) in place across all open and indexed documents; prepareRename validates the target before the operation, prevents renaming of non-variable tokens, and refuses a variable defined outside the workspace (e.g. @{HOME} from the system tunables), which profiles the server cannot see may also use |
| 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 |
| Audit-log rule suggestions | Source code actions generated from AppArmor audit events (DENIED, and complain-mode ALLOWED): the server reads auditd's audit.log or the systemd journal, maps each denial to the corresponding profile rule the same way aa-logprof does, filters out rules the profile (or its included abstractions) already grants, and offers to insert the missing rules — individually or all at once — ranked by severity.db risk score. An information-level diagnostic on the profile header announces how many suggestions are available |
| Code Lens | Summary annotations above key lines: rule and hat counts on profile/hat headers; kernel policy status ("loaded: enforce", "loaded: complain", or "not loaded") read from /sys/kernel/security/apparmor/policy/profiles/; a "modified since last load" indicator when the source file has been edited since the last apparmor_parser load; reference counts on @{VAR} definitions; a clickable lens on profile headers that inserts all rules suggested by audit-log denials (via the apparmor.addAuditRules command); and a clickable lens that deletes every rule flagged as redundant by rule-subsumed / file-rule-subsumed in one edit (via the apparmor.removeSubsumedRules command). Rule/variable counts for resolved include directives are surfaced on hover instead, to avoid noise above profiles that pull in many abstractions. Disable with the apparmor.codeLens.enable setting |
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.
For audit-log rule suggestions the snap needs the log-observe interface
(to run journalctl and read AppArmor denials from the system journal):
sudo snap connect apparmor-language-server:log-observe
For accurate filesystem path completions, connect the system-backup
interface:
sudo snap connect apparmor-language-server:system-backup
The snap reads the host's /etc/apparmor.d (and /etc/apparmor) only. A
profile that includes a file by absolute path elsewhere, such as snapd's
usr.lib.snapd.snap-confine.real with its
#include "/var/lib/snapd/apparmor/snap-confine", cannot be fully checked from
the snap: the include is reported as unreadable. Install from PyPI to check
such profiles.
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) |
-b DIR, --base DIR |
Base directory for resolving include/abi targets (default: /etc/apparmor.d) |
-I DIR, --include-path DIR |
Additional directory to search for include/abi targets, after the base dir (repeatable; when omitted, only the base dir is searched) |
--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.auditLog.enable |
boolean | true |
Enable audit-log rule suggestions. When enabled, code action requests may read AppArmor denial events and offer source actions inserting the missing rules. |
apparmor.auditLog.path |
string | "" |
Path to an audit log file to read denial events from. Leave empty to auto-detect: /var/log/audit/audit.log when readable, otherwise journalctl output for the current boot. |
apparmor.codeLens.enable |
boolean | true |
Show code lenses: rule/hat counts on profile headers, reference counts on variable definitions, audit-log rule suggestions, and subsumed-rule cleanup actions. |
apparmor.severityDbPath |
string | "" |
Path to an AppArmor severity database used to rank rule suggestions. Leave empty to use /etc/apparmor/severity.db. |
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). |
apparmor.abstractionSuggestions.enable |
boolean | true |
Suggest replacing groups of inline rules with an include <abstractions/…> directive when the profile already has rules equivalent to at least coverageThreshold of the abstraction's rules and the abstraction's other rules fall below severityLimit. Suppress for a specific profile with # apparmor-lint: ignore=abstraction-available. |
apparmor.abstractionSuggestions.coverageThreshold |
float | 0.85 |
Fraction (0–1) of an abstraction's rules that the profile must already have (an equivalent rule: neither broader nor narrower) before a suggestion is made. Lower values suggest more abstractions; raise toward 1.0 for fewer, higher-confidence suggestions. |
apparmor.abstractionSuggestions.severityLimit |
integer | 5 |
Maximum severity.db score (0–10) allowed for any rule the abstraction would newly grant. Abstractions whose extra rules exceed this limit are not suggested, preventing silent permission widening. |
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
The extension is published on the
VS Code Marketplace
(publisher AlexMurray) — this is built from the upstream project at
gitlab.com/apparmor/apparmor-language-server/editors/vscode.
Search for AppArmor Language Server in the Extensions view or install from
the command line:
code --install-extension AlexMurray.apparmor-language-server
Alternatively, 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 with the same qualifiers (e.g. audit) 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}/[^.]**. Only reported when no other allow file rule in the profile or its includes could grant a dot file, since the deny rule also masks those |
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 |
file-rule-consolidatable |
Hint | Two or more file rules share the same path and exec target but carry disjoint permission sets that can be merged into a single rule without widening access; conflicting exec modes (e.g. px vs cx) are never flagged |
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 |
rule-consolidatable |
Hint | Two or more network, signal, ptrace, userns, io_uring, mqueue, unix, or dbus rules share the same scalar dimensions (e.g. same peer= or bus=) but differ in at most one list-valued field; they can be merged into a single rule. For rule types with two list fields (e.g. signal permissions and set=), consolidation is only offered when exactly one of the two fields differs — merging both would grant cross-product permissions not present in the originals |
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 | No top-level profile name matches or begins with the document filename (e.g. a file usr.bin.foo should contain a profile /usr/bin/foo; a file foo may contain profiles whose names begin with 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 |
abstraction-available |
Information | Profile's inline rules already include equivalents of ≥85% of an AppArmor abstraction's rules (an abstraction rule broader than the profile's counts as extra), and the abstraction's extra rules are low-severity; suggests replacing the matching rules with include <abstractions/…> |
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:@{VAR}references are resolved against the variable definitions in scope (including the profile-local@{profile_name}and@{attach_path}), so/etc/foo r,is detected as subsumed by@{etc_ro}/foo r,from an included abstraction. Detection stays conservative: a rule is only flagged when every expansion of its path is provably covered (a multi-valued variable that is only partially covered never triggers), and undefined variables are compared as opaque tokens. 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) |
file-rule-consolidatable |
Consolidate file rules for … ⭐ | Merges all file rules for the same path into a single rule with the combined permission set; w subsumes a in the result |
rule-subsumed |
Remove subsumed rule ⭐ | Deletes the redundant rule (and any attached preceding comments) |
rule-consolidatable |
Consolidate … rules ⭐ | Merges all same-type rules that share the same scalar dimensions into a single rule with the union of their list-valued fields |
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 (kept if another rule could grant a dot file) |
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) |
abstraction-available |
Replace with 'include <abstractions/…>' ⭐ | Inserts include <abstractions/name> at the position of the first now-redundant profile rule and deletes all profile rules that the abstraction already covers |
| 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).
Audit-log rule suggestions
Beyond the diagnostic quick fixes above, the server offers source-kind code
actions derived from the system audit log — the same workflow aa-logprof
provides interactively, but inside the editor. With the cursor anywhere in a
profile, request source actions (in VS Code: right-click → Source Action…;
in Neovim: vim.lsp.buf.code_action({ context = { only = { "source" } } })).
When audit events recorded for a profile map to rules it does not already
grant, the server publishes an information-level diagnostic on the profile
header (code audit-log-denials, e.g. "3 rule suggestions available from
7 audit events recorded for this profile — review via source code
actions"), so available suggestions surface in the editor's Problems
panel without any action on your part. Suppress it for a profile with a
# apparmor-lint: ignore=audit-log-denials annotation, or disable the
feature entirely with apparmor.auditLog.enable. The diagnostic is
editor-only: apparmor-lint output never depends on the machine's audit
log.
For every DENIED (enforce mode) or ALLOWED (complain mode) audit event
recorded for the profile being edited, the server suggests the rule that
would permit it:
- an add all exact rules action listed first, then one action per
suggested rule, with rules inserted before a trailing
include if exists <local/…>(or the closing brace); - each file rule is followed by aa-logprof-style widened variants — the
extension-preserving glob (
dir/*.ext), the directory glob (dir/*) and, for deep paths, one level up (parent/**) — so a denial on a single locale file can be resolved with one click as…/locales/** r,instead; exec rules are never widened; - action titles show how many audit events each rule would resolve and the
rule's 0–10 risk rank from
severity.db(the databaseaa-logprofuses); - suggestions already granted by the profile or its included abstractions are
filtered out using the same subsumption logic as the
rule-subsumeddiagnostics (variable-prefixed abstraction rules such as@{etc_ro}/…are expanded before comparison); - the event→rule mapping mirrors
aa-logprof: log-onlyc/dfile mask characters becomew, separate denials on the same path merge into one rule with the combined permissions (anrand awdenial suggest a singlerwrule, andwsubsumesa), matchingfsuid/ouidadds theownerqualifier, deniedexecs are suggested asixtransitions, hex-encoded paths (e.g. containing spaces) are decoded, and paths are generalised to in-scope tunables: well-known prefixes by pattern (/home/<user>→@{HOME},/proc/<pid>→@{PROC}/@{pid},/run/user/<uid>→@{run}/user/@{uid}) plus any path segment equal to a single-value variable's value (e.g./sys/→@{sys}), so suggestions match the tunables the profile already includes; - events from child policy namespaces (e.g. LXD/Docker containers) are
ignored, as are denials on disconnected paths (fixed by the
attach_disconnectedflag, not a rule), hat events when editing the parent profile (and vice versa), and events recorded before the profile file was last modified — an aa-logprof-logmark-style cutoff, so a denial fixed by an earlier edit stops being suggested once the profile is saved.
The log source is auto-detected (/var/log/audit/audit.log when readable,
otherwise the journal) and can be pinned with apparmor.auditLog.path — handy
for logs copied from another machine. Reading the journal typically requires
membership of the adm or systemd-journal group; when running the server
from the snap, the log-observe interface must also be connected (see
From snap); severity ranking reads the host's
/etc/apparmor/severity.db through the automatically connected
hostfs-etc-apparmor interface. Set apparmor.auditLog.enable to false
to disable the feature entirely.
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
Release files for apparmor-language-server 0.9.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| apparmor_language_server-0.9.2.tar.gz | 468.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| apparmor_language_server-0.9.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 735.1 kB
Release files / apparmor_language_server-0.9.2.tar.gz
| Download URL | apparmor_language_server-0.9.2.tar.gz |
|---|---|
| Size | 468.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0374244767dabcb813c1a7d970d2cfbfd3a3175b719fa17f780ee36e5ea3354d
|
|
BLAKE2b-256 checksum How to use checksums |
0a51c28953a11d08e40757add295e6fb380825f83097a7792e885d44d6bea64a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / apparmor_language_server-0.9.2-py3-none-any.whl
| Download URL | apparmor_language_server-0.9.2-py3-none-any.whl |
|---|---|
| Size | 266.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e769cb2ddf4dfdfca28e94f6b4d3a8a522dcaa4b050b85e085ca73a957782327
|
|
BLAKE2b-256 checksum How to use checksums |
0d5f794fbfab72f999269342499cae274dfeb50fb82ff81bf425c168a16a5afc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|