Red-team-friendly inspector for Get-AppLockerPolicy -Effective -Xml output.
- CLI with table / JSON / CSV output and arbitrary filters
- Standalone HTML viewer (drag-n-drop, facets, AND/OR filter groups, presets)
- Risk analyzer with atomic flags — combinations done by filtering, not baked-in
- Diff between two policies (added / removed / modified, identity-aware)
- PowerShell wrapper for capturing live policy
- stdlib-only Python, no external dependencies
Disclaimer. This tool is intended for authorized security assessments, red-team engagements, defensive auditing of your own systems, and CTF / educational use. Do not use it against systems you do not own or have explicit permission to test.
git clone https://github.com/durck/applocker-viewer.git
cd applocker-viewerThat's it — no pip install needed. Python 3.10+ is the only requirement (uses
from __future__ import annotations, PEP 604 unions, etc., available on 3.10+).
Run tests with python -m pytest -q (pytest is the only dev dependency).
applocker.py core parser, filter, analyzer, diff (CLI)
viewer.html standalone single-file viewer (no server, no deps)
Get-AppLockerPolicy.ps1 thin PS wrapper -> applocker.py
tests/ pytest suite + XML fixtures
.\Get-AppLockerPolicy.ps1 analyzepython applocker.py parse policy.xml --pretty
python applocker.py filter policy.xml --action allow --collection exe
python applocker.py filter policy.xml --risky --format json
python applocker.py analyze policy.xml
python applocker.py diff old.xml new.xmlOpen viewer.html in any browser (no server needed). Drag the XML or the JSON
output of applocker.py parse into the page, or click Open file.
The viewer supports:
- Search at the top of the sidebar — case-insensitive substring match
across rule name, description, summary, SID/principal name and serialized
conditions/exceptions (so
mshta,\appdata\,S-1-5-32-544etc. all work). - Filter groups with AND/OR:
- Multi-select inside one facet (e.g.
risk flag) is OR. - Between facets in the same group is AND.
- Between groups is OR (use
+ Add group (OR)). - Tri-state per value: left click = include, right click = exclude.
- Multi-select inside one facet (e.g.
- Built-in presets for common red-team views (broad path allows, LOLBin
allows, user-writable allows, AuditOnly rules, wildcard publishers, …) plus
user-saved presets in
localStorage. Applying a preset replaces the current groups; one-step Undo restores the previous state. - Resizable columns with three gestures, all persisted to
localStorage:- drag right ⇒ pool-compresses everything on the right (widest first), so the right side of the table flattens into a uniform profile and the table stays glued to the section edge;
- drag left ⇒ pool-expands the right side (narrowest first), the symmetric inverse;
- Shift + drag ⇒ free grow (only the dragged column changes; horizontal scroll appears if it overflows);
- double-click the handle ⇒ auto-size the column to its content.
- Resizable filters panel — drag the vertical handle between the sidebar
and the table. Width persists; the panel auto-collapses on narrow viewports
(< 900px) into a
☰ Filtersbutton. - Diff two policies — click
+ Compare, drop a second XML/JSON. The table shows aΔcolumn with+/−/~/=markers, rows are tinted, and modified rules expand to an old → new diff per changed field. AChange typespanel in the sidebar (added / removed / modified / unchanged) is multi-select.EscorExit diffreturns to single mode. - Esc closes the mobile filters drawer first, then exits diff mode.
- Filter, sort and detail state survive across rerenders (scroll positions and search caret are preserved when you toggle facets).
Emit the normalized JSON model.
applocker.py parse <xml|-> [--pretty]
Filter rules and emit table / JSON / CSV.
applocker.py filter <xml|->
[--action allow|deny]
[--collection exe|msi|script|dll|appx]
[--type path|publisher|hash]
[--sid <SID>]
[--search <substring>]
[--has-exceptions | --no-exceptions]
[--risky]
[--flag <risk_flag>]
[--format table|json|csv]
Print a risk-grouped report.
applocker.py analyze <xml|->
Compare two policies. The identity key for pairing rules is the semantic
anchor of the rule — (collection, rule_type, UserOrGroupSid, primary condition value) — so the following are reported as modified (with the
specific changed field listed under changes) instead of as a
removed + added pair:
- a renamed or re-described rule;
- the same rule with
Actionflipped betweenAllowandDenyon the same target; - conditions tweaked (publisher version range narrowed, exception added).
The CLI diff is also available as an interactive mode in viewer.html
(click + Compare).
applocker.py diff <old.xml> <new.xml> [--format text|json]
Every flag is a single observable property, never a combination. Combine
them with filters when you need to express a hypothesis like "broad path AND
allow AND not Everyone" — that's --collection exe --action allow --flag broad_wildcard_path plus filtering by SID, not a single canned flag.
| Flag | Meaning |
|---|---|
audit_only |
Rule lives in a collection with EnforcementMode=AuditOnly |
user_writable_path |
Path covers a known user-writable directory (%TEMP%, %LOCALAPPDATA%, \Windows\Tasks, …) |
broad_wildcard_path |
Path starts with * or contains \*\ (subtree-spanning wildcard, not anchored to a fixed root) |
wildcard_path |
Path contains any * (weaker than broad_wildcard_path; covers C:\App\* and similar trailing wildcards too) |
wildcard_publisher |
Publisher field of a FilePublisher rule is * or empty |
lolbin_path |
Basename matches a curated LOLBin (mshta, regsvr32, msbuild, …) |
has_exceptions |
The rule defines at least one Exceptions entry |
non_everyone_sid |
UserOrGroupSid is not S-1-1-0 (Everyone) |
Plus one policy-level flag, surfaced under metadata.policy_flags:
| Flag | Meaning |
|---|---|
missing_dll_collection |
The policy contains no Dll rules → DLL hijacking is unconstrained |
Atomicity rationale: a rule may have a broad path that is only granted to a specific high-privilege SID. Whether that's exploitable depends on which user context you currently have. Don't pre-bake that decision into the flags — filter for it.
Edit at the top of applocker.py:
USER_WRITABLE_SUBSTRINGS— extend if you have site-specific writable pathsLOLBIN_BASENAMES— sourced from the GTFOBins-style LOLBAS catalog, trim or expand as needed for your engagementWELL_KNOWN_SIDS— built-in SID -> name table for nicer output
risk_flags is added by analyze / filter. parse returns the model
without it (so the parsing layer stays content-only).
- The parser pre-rejects XML containing
DOCTYPEorENTITYdeclarations to prevent XXE through hostile policies. - The HTML viewer mirrors the same XXE check before handing the document to
the browser's
DOMParser. - Both run fully offline; nothing is uploaded.
python -m pytest -q
89 tests covering parser, filters, emitters, analyzer atomicity, and diff identity behaviour (including the action-flip-as-modified contract). CI runs on Linux + Windows across Python 3.10–3.13.
PRs welcome. The code is intentionally compact and stdlib-only — please keep new functionality in the same spirit:
- No external runtime dependencies for
applocker.pyorviewer.html. Tests may use pytest. - Risk flags must stay atomic — one observable property per flag, no combinations. Combinations belong in filter groups in the viewer / CLI.
- Add or extend a test fixture in
tests/fixtures/when introducing new parsing or analysis behaviour. - Run
python -m pytest -qbefore pushing.
- Curated lists (LOLBins, user-writable paths) are deliberately small and conservative — extend them for your environment.
- DLL collection rules are parsed but the analyzer treats absence/0 rules as
missing_dll_collection. Per-rule DLL analysis follows the same shape as Exe rules. Appxpackaged-app conditions are normalized through the same publisher shape; packaged-only attributes that AppLocker may emit are not preserved.
{ "metadata": { "schema_version": 1, "source_file": "policy.xml", "parsed_at": "2026-05-04T09:55:20+00:00", "policy_flags": ["missing_dll_collection"] }, "collections": [ {"type": "Exe", "enforcement_mode": "Enabled", "rule_count": 9} ], "rules": [ { "id": "e0771f6b5e2eb8f5", "collection": "Exe", "rule_type": "FilePath", "action": "Allow", "name": "(Default Rule) All files", "description": "", "user_or_group_sid": "S-1-1-0", "user_or_group_name": "Everyone", "conditions": [{"type": "path", "path": "*"}], "exceptions": [], "summary": "*", "risk_flags": ["broad_wildcard_path"] } ] }