Skip to main content

AppArmor Language Server (apparmor-language-server)

apparmor-language-server PyPI - Version VS Code Marketplace 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; 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.

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)
-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:

  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 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-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: @{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* 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)
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 database aa-logprof uses);
  • suggestions already granted by the profile or its included abstractions are filtered out using the same subsumption logic as the rule-subsumed diagnostics (variable-prefixed abstraction rules such as @{etc_ro}/… are expanded before comparison);
  • the event→rule mapping mirrors aa-logprof: log-only c/d file mask characters become w, separate denials on the same path merge into one rule with the combined permissions (an r and a w denial suggest a single rw rule, and w subsumes a), matching fsuid/ouid adds the owner qualifier, denied execs are suggested as ix transitions, 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_disconnected flag, 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)

Source distribution for apparmor-language-server 0.9.2
File Size Uploaded
apparmor_language_server-0.9.2.tar.gz 468.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for apparmor-language-server 0.9.2
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.9.2 This release

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page